Compare commits

..

No commits in common. "dev" and "v1.3.5" have entirely different histories.
dev ... v1.3.5

308 changed files with 41283 additions and 6049 deletions

View file

@ -13,23 +13,12 @@ on:
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
runs-on: macos-latest
strategy:
matrix:
qt-ver: [5.15.2, 6.5.3]
# 配置qt-ver的额外设置qt-arch-installbuild-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
qt-ver: [5.12.6]
qt-arch-install: [clang_64]
clang-arch: [x64]
env:
target-name: QtScrcpy
qt-install-path: ${{ github.workspace }}/${{ matrix.qt-ver }}
@ -37,35 +26,25 @@ jobs:
steps:
- name: Cache Qt
id: cache-qt
uses: actions/cache@v4
uses: actions/cache@v1
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
- name: Install Qt
uses: jurplel/install-qt-action@v2.6.2
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
- uses: actions/checkout@v1
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 }}
fetch-depth: 1
# 编译
- 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 }}
ci/mac/build_for_mac.sh release
# 获取ref最后一个/后的内容
- name: Get the version
shell: bash
@ -77,17 +56,17 @@ jobs:
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 }}
publish_name: ${{ env.target-name }}-${{ env.plantform-des }}-${{ matrix.clang-arch }}-${{ steps.get-version.outputs.version }}
run: |
ci/mac/publish_for_mac.sh ../build ${{ matrix.build-arch }}
ci/mac/publish_for_mac.sh ../build
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
- uses: actions/upload-artifact@v1
with:
name: ${{ steps.package.outputs.package-name }}.zip
path: ci/build/${{ steps.package.outputs.package-name }}.dmg
path: ci/build/${{ steps.package.outputs.package-name }}.app
# Upload to release
- name: Upload Release
if: startsWith(github.ref, 'refs/tags/')

View file

@ -1,11 +1,11 @@
name: Ubuntu
# Qt官方没有linux平台的x86包
on:
push:
paths:
- 'QtScrcpy/**'
- '!QtScrcpy/res/**'
- '.github/workflows/ubuntu.yml'
- 'ci/linux/**'
pull_request:
paths:
- 'QtScrcpy/**'
@ -17,8 +17,8 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-22.04]
qt-ver: [5.15.2]
os: [ubuntu-16.04,ubuntu-18.04]
qt-ver: [5.12.6]
qt-arch-install: [gcc_64]
gcc-arch: [x64]
env:
@ -26,56 +26,25 @@ jobs:
qt-install-path: ${{ github.workspace }}/${{ matrix.qt-ver }}
plantform-des: ubuntu
steps:
- name: Install Qt
uses: jurplel/install-qt-action@v4.1.1
with:
version: ${{ matrix.qt-ver }}
cached: ${{ steps.cache-qt.outputs.cache-hit }}
- name: Cache Qt
id: cache-qt
uses: actions/cache@v4
uses: actions/cache@v1
with:
path: ${{ env.qt-install-path }}/${{ matrix.qt-arch-install }}
key: ${{ runner.os }}/${{ matrix.qt-ver }}/${{ matrix.qt-arch-install }}
- name: Install GL library
- name: Install Qt
uses: jurplel/install-qt-action@v2.6.2
with:
version: ${{ matrix.qt-ver }}
cached: ${{ steps.cache-qt.outputs.cache-hit }}
- name: Ubuntu install GL library
run: sudo apt-get install -y libglew-dev libglfw3-dev
- uses: actions/checkout@v2
- uses: actions/checkout@v1
with:
fetch-depth: 0
submodules: 'true'
ssh-key: ${{ secrets.BOT_SSH_KEY }}
- name: Build RelWithDebInfo
fetch-depth: 1
- name: Build Ubuntu
env:
ENV_QT_PATH: ${{ env.qt-install-path }}
run: |
ci/linux/build_for_linux.sh "RelWithDebInfo"
- name: Upload RelWithDebInfo
uses: actions/upload-artifact@v4
with:
name: QtScrcpy-${{ matrix.os }}-${{ matrix.qt-arch-install }}-RelWithDebInfo
path: output/x64/RelWithDebInfo/*
- name: Build Release
env:
ENV_QT_PATH: ${{ env.qt-install-path }}
run: |
ci/linux/build_for_linux.sh "Release"
- name: Upload Release
uses: actions/upload-artifact@v4
with:
name: QtScrcpy-${{ matrix.os }}-${{ matrix.qt-arch-install }}-Release
path: output/x64/Release/*
- name: Install the zip utility
run: |
sudo apt install zip -y
- name: Zip the Artifacts
run: |
zip -r QtScrcpy-${{ matrix.os }}-${{ matrix.qt-arch-install }}.zip output/x64/Release
- name: Upload to Releases
if: startsWith(github.ref, 'refs/tags/')
uses: svenstaro/upload-release-action@2.3.0
with:
file: QtScrcpy-${{ matrix.os }}-${{ matrix.qt-arch-install }}.zip
asset_name: QtScrcpy-${{ matrix.os }}-${{ matrix.qt-arch-install }}.zip
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
overwrite: true
python ci/generate-version.py
ci/linux/build_for_ubuntu.sh release

View file

@ -6,55 +6,51 @@ on:
- '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 2019
# windows server 2019安装的是vs2019windows server 2016安装的是vs2017
# windows-latest目前是windows server 2019,选择2016是2016安装的是vs2017
# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on
runs-on: windows-2019
runs-on: windows-2016
# 矩阵配置 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-ver: [5.12.6]
qt-arch: [win64_msvc2017_64, win32_msvc2017]
# 配置qt-arch的额外设置msvc-archqt-arch-install
include:
- qt-arch: win64_msvc2019_64
- qt-arch: win64_msvc2017_64
msvc-arch: x64
qt-arch-install: msvc2019_64
- qt-arch: win32_msvc2019
qt-arch-install: msvc2017_64
- qt-arch: win32_msvc2017
msvc-arch: x86
qt-arch-install: msvc2019
qt-arch-install: msvc2017
# 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\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat'
vcinstall-path: 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC'
vcvarsall-path: 'C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\VC\Auxiliary\Build\vcvarsall.bat'
qt-install-path: ${{ github.workspace }}/${{ matrix.qt-ver }}
plantform-des: win
# 步骤
steps:
- name: Cache Qt
id: cache-qt
uses: actions/cache@v4
uses: actions/cache@v1
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
uses: jurplel/install-qt-action@v2.6.2
with:
# Version of Qt to install
version: ${{ matrix.qt-ver }}
@ -64,11 +60,9 @@ jobs:
arch: ${{ matrix.qt-arch }}
cached: ${{ steps.cache-qt.outputs.cache-hit }}
# 拉取代码
- uses: actions/checkout@v2
- uses: actions/checkout@v1
with:
fetch-depth: 0
submodules: 'true'
ssh-key: ${{ secrets.BOT_SSH_KEY }}
fetch-depth: 1
# 编译msvc
- name: Build MSVC
# shell介绍 https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell
@ -78,7 +72,7 @@ jobs:
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 }}
call "ci\win\build_for_win.bat" release ${{ matrix.msvc-arch }}
# 获取ref最后一个/后的内容
- name: Get the version
shell: bash
@ -89,8 +83,6 @@ jobs:
- 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: |
@ -100,7 +92,7 @@ jobs:
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
- uses: actions/upload-artifact@v1
with:
name: ${{ steps.package.outputs.package-name }}.zip
path: ci\build\${{ steps.package.outputs.package-name }}

4
.gitignore vendored
View file

@ -12,6 +12,4 @@
/build/
build-*
*.DS_Store
userdata.ini
Info_Mac.plist
/ci/build_temp
userdata.ini

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "QtScrcpy/QtScrcpyCore"]
path = QtScrcpy/QtScrcpyCore
url = git@github.com:barry-ran/QtScrcpyCore.git

View file

@ -1,4 +0,0 @@
cmake_minimum_required(VERSION 3.19 FATAL_ERROR)
project(all)
add_subdirectory(QtScrcpy)

View file

@ -186,8 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (C) 2019 Rankun
Copyright (C) 2019-2025 Rankun
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -1,382 +0,0 @@
# 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}")
# 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})
#
# 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
)

214
QtScrcpy/QtScrcpy.pro Normal file
View file

@ -0,0 +1,214 @@
#-------------------------------------------------
#
# Project created by QtCreator 2018-10-07T12:36:10
#
#-------------------------------------------------
QT += core gui
QT += network
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = QtScrcpy
TEMPLATE = app
# The following define makes your compiler emit warnings if you use
# any feature of Qt which has been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
msvc{
QMAKE_CFLAGS += -source-charset:utf-8
QMAKE_CXXFLAGS += -source-charset:utf-8
}
# warning as error
#4566 https://github.com/Chuyu-Team/VC-LTL/issues/27
*g++*: QMAKE_CXXFLAGS += -Werror
*msvc*: QMAKE_CXXFLAGS += /WX /wd4566
# 源码
SOURCES += \
main.cpp \
dialog.cpp
HEADERS += \
dialog.h
FORMS += \
dialog.ui
# 子工程
include ($$PWD/common/common.pri)
include ($$PWD/adb/adb.pri)
include ($$PWD/uibase/uibase.pri)
include ($$PWD/fontawesome/fontawesome.pri)
include ($$PWD/util/util.pri)
include ($$PWD/device/device.pri)
include ($$PWD/devicemanage/devicemanage.pri)
# 附加包含路径
INCLUDEPATH += \
$$PWD/common \
$$PWD/adb \
$$PWD/uibase \
$$PWD/util \
$$PWD/device \
$$PWD/devicemanage \
$$PWD/fontawesome
# 如果变量没有定义
# !defined(TEST_VAR, var) {
# message("test")
# }
# 从文件读取版本号
CAT_VERSION = $$cat($$PWD/version)
# 拆分出版本号
VERSION_MAJOR = $$section(CAT_VERSION, ., 0, 0)
VERSION_MINOR = $$section(CAT_VERSION, ., 1, 1)
VERSION_PATCH = $$section(CAT_VERSION, ., 2, 2)
message("version:" $${VERSION_MAJOR}.$${VERSION_MINOR}.$${VERSION_PATCH})
# qmake变量的方式定义版本号
VERSION = $${VERSION_MAJOR}.$${VERSION_MINOR}.$${VERSION_PATCH}
# ***********************************************************
# Win平台下配置
# ***********************************************************
win32 {
# 通过rc的方式的话VERSION变量rc中获取不到,定义为宏方便rc中使用
DEFINES += VERSION_MAJOR=$${VERSION_MAJOR}
DEFINES += VERSION_MINOR=$${VERSION_MINOR}
DEFINES += VERSION_PATCH=$${VERSION_PATCH}
DEFINES += VERSION_RC_STR=\\\"$${VERSION_MAJOR}.$${VERSION_MINOR}.$${VERSION_PATCH}\\\"
contains(QT_ARCH, x86_64) {
message("x64")
# 输出目录
CONFIG(debug, debug|release) {
DESTDIR = $$PWD/../output/win/x64/debug
} else {
DESTDIR = $$PWD/../output/win/x64/release
}
# 依赖模块
LIBS += \
-L$$PWD/../third_party/ffmpeg/lib/x64 -lavformat \
-L$$PWD/../third_party/ffmpeg/lib/x64 -lavcodec \
-L$$PWD/../third_party/ffmpeg/lib/x64 -lavutil \
-L$$PWD/../third_party/ffmpeg/lib/x64 -lswscale
WIN_FFMPEG_SRC = $$PWD/../third_party/ffmpeg/bin/x64/*.dll
} else {
message("x86")
# 输出目录
CONFIG(debug, debug|release) {
DESTDIR = $$PWD/../output/win/x86/debug
} else {
DESTDIR = $$PWD/../output/win/x86/release
}
# 依赖模块
LIBS += \
-L$$PWD/../third_party/ffmpeg/lib/x86 -lavformat \
-L$$PWD/../third_party/ffmpeg/lib/x86 -lavcodec \
-L$$PWD/../third_party/ffmpeg/lib/x86 -lavutil \
-L$$PWD/../third_party/ffmpeg/lib/x86 -lswscale
WIN_FFMPEG_SRC = $$PWD/../third_party/ffmpeg/bin/x86/*.dll
}
# 复制依赖库
WIN_DST = $$DESTDIR
WIN_FFMPEG_SRC ~= s,/,\\,g
WIN_DST ~= s,/,\\,g
QMAKE_POST_LINK += $$quote($$QMAKE_COPY $$WIN_FFMPEG_SRC $$WIN_DST$$escape_expand(\n\t))
# windows rc file
RC_FILE = $$PWD/res/QtScrcpy.rc
}
# ***********************************************************
# Mac平台下配置
# ***********************************************************
macos {
# 输出目录
CONFIG(debug, debug|release) {
DESTDIR = $$PWD/../output/mac/debug
} else {
DESTDIR = $$PWD/../output/mac/release
}
# 依赖模块
LIBS += \
-L$$PWD/../third_party/ffmpeg/lib -lavformat.58 \
-L$$PWD/../third_party/ffmpeg/lib -lavcodec.58 \
-L$$PWD/../third_party/ffmpeg/lib -lavutil.56 \
-L$$PWD/../third_party/ffmpeg/lib -lswscale.5
# mac bundle file
APP_SCRCPY_SERVER.files = $$files($$PWD/../third_party/scrcpy-server)
APP_SCRCPY_SERVER.path = Contents/MacOS
QMAKE_BUNDLE_DATA += APP_SCRCPY_SERVER
APP_ADB.files = $$files($$PWD/../third_party/adb/mac/adb)
APP_ADB.path = Contents/MacOS
QMAKE_BUNDLE_DATA += APP_ADB
APP_FFMPEG.files = $$files($$PWD/../third_party/ffmpeg/lib/*.dylib)
APP_FFMPEG.path = Contents/MacOS
QMAKE_BUNDLE_DATA += APP_FFMPEG
APP_CONFIG.files = $$files($$PWD/../config/config.ini)
APP_CONFIG.path = Contents/MacOS/config
QMAKE_BUNDLE_DATA += APP_CONFIG
# mac application icon
ICON = $$PWD/res/QtScrcpy.icns
QMAKE_INFO_PLIST = $$PWD/res/Info_Mac.plist
# 定义目标命令(修改版本号字段)
plistupdate.commands = /usr/libexec/PlistBuddy -c \"Set :CFBundleShortVersionString $$VERSION\" \
-c \"Set :CFBundleVersion $$VERSION\" \
$$DESTDIR/$${TARGET}.app/Contents/Info.plist
# 增加额外目标
QMAKE_EXTRA_TARGETS += plistupdate
# 设置为前置依赖
PRE_TARGETDEPS += plistupdate
}
# ***********************************************************
# Linux平台下配置
# ***********************************************************
linux {
# 输出目录
CONFIG(debug, debug|release) {
DESTDIR = $$PWD/../output/linux/debug
} else {
DESTDIR = $$PWD/../output/linux/release
}
# 依赖模块
LIBS += \
-L$$PWD/../third_party/ffmpeg/lib -lavformat \
-L$$PWD/../third_party/ffmpeg/lib -lavcodec \
-L$$PWD/../third_party/ffmpeg/lib -lavutil \
-L$$PWD/../third_party/ffmpeg/lib -lswscale
# linux set app icon: https://blog.csdn.net/MrNoboday/article/details/82870853
}
# message("test")
RESOURCES += \
res/res.qrc

@ -1 +0,0 @@
Subproject commit 19e1ba8fb5c59c5a85c3c6a79967fab4c84739c7

5
QtScrcpy/adb/adb.pri Normal file
View file

@ -0,0 +1,5 @@
HEADERS += \
$$PWD/adbprocess.h
SOURCES += \
$$PWD/adbprocess.cpp

236
QtScrcpy/adb/adbprocess.cpp Normal file
View file

@ -0,0 +1,236 @@
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QProcess>
#include "adbprocess.h"
#include "config.h"
QString AdbProcess::s_adbPath = "";
AdbProcess::AdbProcess(QObject *parent) : QProcess(parent)
{
initSignals();
}
AdbProcess::~AdbProcess()
{
if (isRuning()) {
close();
}
}
const QString &AdbProcess::getAdbPath()
{
if (s_adbPath.isEmpty()) {
s_adbPath = QString::fromLocal8Bit(qgetenv("QTSCRCPY_ADB_PATH"));
QFileInfo fileInfo(s_adbPath);
if (s_adbPath.isEmpty() || !fileInfo.isFile()) {
s_adbPath = Config::getInstance().getAdbPath();
}
fileInfo = s_adbPath;
if (s_adbPath.isEmpty() || !fileInfo.isFile()) {
s_adbPath = QCoreApplication::applicationDirPath() + "/adb";
}
qInfo("adb path: %s", QDir(s_adbPath).absolutePath().toUtf8().data());
}
return s_adbPath;
}
void AdbProcess::initSignals()
{
// aboutToQuit not exit event loop, so deletelater is ok
//connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &AdbProcess::deleteLater);
connect(this, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
if (NormalExit == exitStatus && 0 == exitCode) {
emit adbProcessResult(AER_SUCCESS_EXEC);
} else {
//P7C0218510000537 unauthorized ,手机端此时弹出调试认证,要允许调试
emit adbProcessResult(AER_ERROR_EXEC);
}
qDebug() << "adb return " << exitCode << "exit status " << exitStatus;
});
connect(this, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) {
if (QProcess::FailedToStart == error) {
emit adbProcessResult(AER_ERROR_MISSING_BINARY);
} else {
emit adbProcessResult(AER_ERROR_START);
QString err = QString("qprocess start error:%1 %2").arg(program()).arg(arguments().join(" "));
qCritical() << err.toStdString().c_str();
}
});
connect(this, &QProcess::readyReadStandardError, this, [this]() {
QString tmp = QString::fromUtf8(readAllStandardError()).trimmed();
m_errorOutput += tmp;
qWarning() << QString("AdbProcess::error:%1").arg(tmp).toStdString().data();
});
connect(this, &QProcess::readyReadStandardOutput, this, [this]() {
QString tmp = QString::fromUtf8(readAllStandardOutput()).trimmed();
m_standardOutput += tmp;
qInfo() << QString("AdbProcess::out:%1").arg(tmp).toStdString().data();
});
connect(this, &QProcess::started, this, [this]() { emit adbProcessResult(AER_SUCCESS_START); });
}
void AdbProcess::execute(const QString &serial, const QStringList &args)
{
m_standardOutput = "";
m_errorOutput = "";
QStringList adbArgs;
if (!serial.isEmpty()) {
adbArgs << "-s" << serial;
}
adbArgs << args;
qDebug() << getAdbPath() << adbArgs.join(" ");
start(getAdbPath(), adbArgs);
}
bool AdbProcess::isRuning()
{
if (QProcess::NotRunning == state()) {
return false;
} else {
return true;
}
}
void AdbProcess::setShowTouchesEnabled(const QString &serial, bool enabled)
{
QStringList adbArgs;
adbArgs << "shell"
<< "settings"
<< "put"
<< "system"
<< "show_touches";
adbArgs << (enabled ? "1" : "0");
execute(serial, adbArgs);
}
QStringList AdbProcess::getDevicesSerialFromStdOut()
{
// get devices serial by adb devices
QStringList serials;
QStringList devicesInfoList = m_standardOutput.split(QRegExp("\r\n|\n"), QString::SkipEmptyParts);
for (QString deviceInfo : devicesInfoList) {
QStringList deviceInfos = deviceInfo.split(QRegExp("\t"), QString::SkipEmptyParts);
if (2 == deviceInfos.count() && 0 == deviceInfos[1].compare("device")) {
serials << deviceInfos[0];
}
}
return serials;
}
QString AdbProcess::getDeviceIPFromStdOut()
{
QString ip = "";
#if 0
QString strIPExp = "inet [\\d.]*";
QRegExp ipRegExp(strIPExp,Qt::CaseInsensitive);
if (ipRegExp.indexIn(m_standardOutput) != -1) {
ip = ipRegExp.cap(0);
ip = ip.right(ip.size() - 5);
}
#else
QString strIPExp = "inet addr:[\\d.]*";
QRegExp ipRegExp(strIPExp, Qt::CaseInsensitive);
if (ipRegExp.indexIn(m_standardOutput) != -1) {
ip = ipRegExp.cap(0);
ip = ip.right(ip.size() - 10);
}
#endif
return ip;
}
QString AdbProcess::getDeviceIPByIpFromStdOut()
{
QString ip = "";
QString strIPExp = "wlan0 inet [\\d.]*";
QRegExp ipRegExp(strIPExp, Qt::CaseInsensitive);
if (ipRegExp.indexIn(m_standardOutput) != -1) {
ip = ipRegExp.cap(0);
ip = ip.right(ip.size() - 14);
}
qDebug() << "get ip: " << ip;
return ip;
}
QString AdbProcess::getStdOut()
{
return m_standardOutput;
}
QString AdbProcess::getErrorOut()
{
return m_errorOutput;
}
void AdbProcess::forward(const QString &serial, quint16 localPort, const QString &deviceSocketName)
{
QStringList adbArgs;
adbArgs << "forward";
adbArgs << QString("tcp:%1").arg(localPort);
adbArgs << QString("localabstract:%1").arg(deviceSocketName);
execute(serial, adbArgs);
}
void AdbProcess::forwardRemove(const QString &serial, quint16 localPort)
{
QStringList adbArgs;
adbArgs << "forward";
adbArgs << "--remove";
adbArgs << QString("tcp:%1").arg(localPort);
execute(serial, adbArgs);
}
void AdbProcess::reverse(const QString &serial, const QString &deviceSocketName, quint16 localPort)
{
QStringList adbArgs;
adbArgs << "reverse";
adbArgs << QString("localabstract:%1").arg(deviceSocketName);
adbArgs << QString("tcp:%1").arg(localPort);
execute(serial, adbArgs);
}
void AdbProcess::reverseRemove(const QString &serial, const QString &deviceSocketName)
{
QStringList adbArgs;
adbArgs << "reverse";
adbArgs << "--remove";
adbArgs << QString("localabstract:%1").arg(deviceSocketName);
execute(serial, adbArgs);
}
void AdbProcess::push(const QString &serial, const QString &local, const QString &remote)
{
QStringList adbArgs;
adbArgs << "push";
adbArgs << local;
adbArgs << remote;
execute(serial, adbArgs);
}
void AdbProcess::install(const QString &serial, const QString &local)
{
QStringList adbArgs;
adbArgs << "install";
adbArgs << "-r";
adbArgs << local;
execute(serial, adbArgs);
}
void AdbProcess::removePath(const QString &serial, const QString &path)
{
QStringList adbArgs;
adbArgs << "shell";
adbArgs << "rm";
adbArgs << path;
execute(serial, adbArgs);
}

53
QtScrcpy/adb/adbprocess.h Normal file
View file

@ -0,0 +1,53 @@
#ifndef ADBPROCESS_H
#define ADBPROCESS_H
#include <QProcess>
class AdbProcess : public QProcess
{
Q_OBJECT
public:
enum ADB_EXEC_RESULT
{
AER_SUCCESS_START, // 启动成功
AER_ERROR_START, // 启动失败
AER_SUCCESS_EXEC, // 执行成功
AER_ERROR_EXEC, // 执行失败
AER_ERROR_MISSING_BINARY, // 找不到文件
};
explicit AdbProcess(QObject *parent = nullptr);
virtual ~AdbProcess();
void execute(const QString &serial, const QStringList &args);
void forward(const QString &serial, quint16 localPort, const QString &deviceSocketName);
void forwardRemove(const QString &serial, quint16 localPort);
void reverse(const QString &serial, const QString &deviceSocketName, quint16 localPort);
void reverseRemove(const QString &serial, const QString &deviceSocketName);
void push(const QString &serial, const QString &local, const QString &remote);
void install(const QString &serial, const QString &local);
void removePath(const QString &serial, const QString &path);
bool isRuning();
void setShowTouchesEnabled(const QString &serial, bool enabled);
QStringList getDevicesSerialFromStdOut();
QString getDeviceIPFromStdOut();
QString getDeviceIPByIpFromStdOut();
QString getStdOut();
QString getErrorOut();
static const QString &getAdbPath();
signals:
void adbProcessResult(ADB_EXEC_RESULT processResult);
private:
void initSignals();
private:
QString m_standardOutput = "";
QString m_errorOutput = "";
static QString s_adbPath;
};
#endif // ADBPROCESS_H

View file

@ -1,235 +0,0 @@
#include <QTcpSocket>
#include <QHostAddress>
#include <QAudioOutput>
#include <QTime>
#include <QElapsedTimer>
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
#include <QAudioSink>
#include <QAudioDevice>
#include <QMediaDevices>
#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.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<QAbstractSocket::SocketError>::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();
}

View file

@ -1,46 +0,0 @@
#ifndef AUDIOOUTPUT_H
#define AUDIOOUTPUT_H
#include <QThread>
#include <QProcess>
#include <QPointer>
#include <QVector>
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<QIODevice> m_outputDevice;
QThread m_workerThread;
QProcess m_sndcpy;
QVector<char> 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

View file

@ -0,0 +1,2 @@
HEADERS += \
$$PWD/qscrcpyevent.h

View file

@ -0,0 +1,22 @@
#ifndef QSCRCPYEVENT_H
#define QSCRCPYEVENT_H
#include <QEvent>
class QScrcpyEvent : public QEvent
{
public:
enum Type
{
VideoSocket = QEvent::User + 1,
Control,
};
QScrcpyEvent(Type type) : QEvent(QEvent::Type(type)) {}
};
// VideoSocketEvent
class VideoSocketEvent : public QScrcpyEvent
{
public:
VideoSocketEvent() : QScrcpyEvent(VideoSocket) {}
};
#endif // QSCRCPYEVENT_H

View file

@ -0,0 +1,3 @@
HEADERS += \
$$PWD/input.h \
$$PWD/keycodes.h

View file

@ -0,0 +1,840 @@
// copied from <https://android.googlesource.com/platform/frameworks/native/+/master/include/android/input.h>
// blob 08299899b6305a0fe74d7d2b8471b7cd0af49dc7
// (and modified)
/*
* Copyright (C) 2010 The Android Open Source Project
*
* 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.
*/
#ifndef _ANDROID_INPUT_H
#define _ANDROID_INPUT_H
/**
* Meta key / modifer state.
*/
enum AndroidMetastate
{
/** No meta keys are pressed. */
AMETA_NONE = 0,
/** This mask is used to check whether one of the ALT meta keys is pressed. */
AMETA_ALT_ON = 0x02,
/** This mask is used to check whether the left ALT meta key is pressed. */
AMETA_ALT_LEFT_ON = 0x10,
/** This mask is used to check whether the right ALT meta key is pressed. */
AMETA_ALT_RIGHT_ON = 0x20,
/** This mask is used to check whether one of the SHIFT meta keys is pressed. */
AMETA_SHIFT_ON = 0x01,
/** This mask is used to check whether the left SHIFT meta key is pressed. */
AMETA_SHIFT_LEFT_ON = 0x40,
/** This mask is used to check whether the right SHIFT meta key is pressed. */
AMETA_SHIFT_RIGHT_ON = 0x80,
/** This mask is used to check whether the SYM meta key is pressed. */
AMETA_SYM_ON = 0x04,
/** This mask is used to check whether the FUNCTION meta key is pressed. */
AMETA_FUNCTION_ON = 0x08,
/** This mask is used to check whether one of the CTRL meta keys is pressed. */
AMETA_CTRL_ON = 0x1000,
/** This mask is used to check whether the left CTRL meta key is pressed. */
AMETA_CTRL_LEFT_ON = 0x2000,
/** This mask is used to check whether the right CTRL meta key is pressed. */
AMETA_CTRL_RIGHT_ON = 0x4000,
/** This mask is used to check whether one of the META meta keys is pressed. */
AMETA_META_ON = 0x10000,
/** This mask is used to check whether the left META meta key is pressed. */
AMETA_META_LEFT_ON = 0x20000,
/** This mask is used to check whether the right META meta key is pressed. */
AMETA_META_RIGHT_ON = 0x40000,
/** This mask is used to check whether the CAPS LOCK meta key is on. */
AMETA_CAPS_LOCK_ON = 0x100000,
/** This mask is used to check whether the NUM LOCK meta key is on. */
AMETA_NUM_LOCK_ON = 0x200000,
/** This mask is used to check whether the SCROLL LOCK meta key is on. */
AMETA_SCROLL_LOCK_ON = 0x400000,
};
/**
* Input event types.
*/
enum AndroidInputEventType
{
/** Indicates that the input event is a key event. */
AINPUT_EVENT_TYPE_KEY = 1,
/** Indicates that the input event is a motion event. */
AINPUT_EVENT_TYPE_MOTION = 2
};
/**
* Key event actions.
*/
enum AndroidKeyeventAction
{
/** The key has been pressed down. */
AKEY_EVENT_ACTION_DOWN = 0,
/** The key has been released. */
AKEY_EVENT_ACTION_UP = 1,
/**
* Multiple duplicate key events have occurred in a row, or a
* complex string is being delivered. The repeat_count property
* of the key event contains the number of times the given key
* code should be executed.
*/
AKEY_EVENT_ACTION_MULTIPLE = 2
};
/**
* Key event flags.
*/
enum AndroidKeyeventFlags
{
/** This mask is set if the device woke because of this key event. */
AKEY_EVENT_FLAG_WOKE_HERE = 0x1,
/** This mask is set if the key event was generated by a software keyboard. */
AKEY_EVENT_FLAG_SOFT_KEYBOARD = 0x2,
/** This mask is set if we don't want the key event to cause us to leave touch mode. */
AKEY_EVENT_FLAG_KEEP_TOUCH_MODE = 0x4,
/**
* This mask is set if an event was known to come from a trusted
* part of the system. That is, the event is known to come from
* the user, and could not have been spoofed by a third party
* component.
*/
AKEY_EVENT_FLAG_FROM_SYSTEM = 0x8,
/**
* This mask is used for compatibility, to identify enter keys that are
* coming from an IME whose enter key has been auto-labelled "next" or
* "done". This allows TextView to dispatch these as normal enter keys
* for old applications, but still do the appropriate action when
* receiving them.
*/
AKEY_EVENT_FLAG_EDITOR_ACTION = 0x10,
/**
* When associated with up key events, this indicates that the key press
* has been canceled. Typically this is used with virtual touch screen
* keys, where the user can slide from the virtual key area on to the
* display: in that case, the application will receive a canceled up
* event and should not perform the action normally associated with the
* key. Note that for this to work, the application can not perform an
* action for a key until it receives an up or the long press timeout has
* expired.
*/
AKEY_EVENT_FLAG_CANCELED = 0x20,
/**
* This key event was generated by a virtual (on-screen) hard key area.
* Typically this is an area of the touchscreen, outside of the regular
* display, dedicated to "hardware" buttons.
*/
AKEY_EVENT_FLAG_VIRTUAL_HARD_KEY = 0x40,
/**
* This flag is set for the first key repeat that occurs after the
* long press timeout.
*/
AKEY_EVENT_FLAG_LONG_PRESS = 0x80,
/**
* Set when a key event has AKEY_EVENT_FLAG_CANCELED set because a long
* press action was executed while it was down.
*/
AKEY_EVENT_FLAG_CANCELED_LONG_PRESS = 0x100,
/**
* Set for AKEY_EVENT_ACTION_UP when this event's key code is still being
* tracked from its initial down. That is, somebody requested that tracking
* started on the key down and a long press has not caused
* the tracking to be canceled.
*/
AKEY_EVENT_FLAG_TRACKING = 0x200,
/**
* Set when a key event has been synthesized to implement default behavior
* for an event that the application did not handle.
* Fallback key events are generated by unhandled trackball motions
* (to emulate a directional keypad) and by certain unhandled key presses
* that are declared in the key map (such as special function numeric keypad
* keys when numlock is off).
*/
AKEY_EVENT_FLAG_FALLBACK = 0x400,
};
/**
* Bit shift for the action bits holding the pointer index as
* defined by AMOTION_EVENT_ACTION_POINTER_INDEX_MASK.
*/
#define AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT 8
/** Motion event actions */
enum AndroidMotioneventAction
{
/** Bit mask of the parts of the action code that are the action itself. */
AMOTION_EVENT_ACTION_MASK = 0xff,
/**
* Bits in the action code that represent a pointer index, used with
* AMOTION_EVENT_ACTION_POINTER_DOWN and AMOTION_EVENT_ACTION_POINTER_UP. Shifting
* down by AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT provides the actual pointer
* index where the data for the pointer going up or down can be found.
*/
AMOTION_EVENT_ACTION_POINTER_INDEX_MASK = 0xff00,
/** A pressed gesture has started, the motion contains the initial starting location. */
AMOTION_EVENT_ACTION_DOWN = 0,
/**
* A pressed gesture has finished, the motion contains the final release location
* as well as any intermediate points since the last down or move event.
*/
AMOTION_EVENT_ACTION_UP = 1,
/**
* A change has happened during a press gesture (between AMOTION_EVENT_ACTION_DOWN and
* AMOTION_EVENT_ACTION_UP). The motion contains the most recent point, as well as
* any intermediate points since the last down or move event.
*/
AMOTION_EVENT_ACTION_MOVE = 2,
/**
* The current gesture has been aborted.
* You will not receive any more points in it. You should treat this as
* an up event, but not perform any action that you normally would.
*/
AMOTION_EVENT_ACTION_CANCEL = 3,
/**
* A movement has happened outside of the normal bounds of the UI element.
* This does not provide a full gesture, but only the initial location of the movement/touch.
*/
AMOTION_EVENT_ACTION_OUTSIDE = 4,
/**
* A non-primary pointer has gone down.
* The bits in AMOTION_EVENT_ACTION_POINTER_INDEX_MASK indicate which pointer changed.
*/
AMOTION_EVENT_ACTION_POINTER_DOWN = 5,
/**
* A non-primary pointer has gone up.
* The bits in AMOTION_EVENT_ACTION_POINTER_INDEX_MASK indicate which pointer changed.
*/
AMOTION_EVENT_ACTION_POINTER_UP = 6,
/**
* A change happened but the pointer is not down (unlike AMOTION_EVENT_ACTION_MOVE).
* The motion contains the most recent point, as well as any intermediate points since
* the last hover move event.
*/
AMOTION_EVENT_ACTION_HOVER_MOVE = 7,
/**
* The motion event contains relative vertical and/or horizontal scroll offsets.
* Use getAxisValue to retrieve the information from AMOTION_EVENT_AXIS_VSCROLL
* and AMOTION_EVENT_AXIS_HSCROLL.
* The pointer may or may not be down when this event is dispatched.
* This action is always delivered to the winder under the pointer, which
* may not be the window currently touched.
*/
AMOTION_EVENT_ACTION_SCROLL = 8,
/** The pointer is not down but has entered the boundaries of a window or view. */
AMOTION_EVENT_ACTION_HOVER_ENTER = 9,
/** The pointer is not down but has exited the boundaries of a window or view. */
AMOTION_EVENT_ACTION_HOVER_EXIT = 10,
/* One or more buttons have been pressed. */
AMOTION_EVENT_ACTION_BUTTON_PRESS = 11,
/* One or more buttons have been released. */
AMOTION_EVENT_ACTION_BUTTON_RELEASE = 12,
};
/**
* Motion event flags.
*/
enum AndroidMotioneventFlags
{
/**
* This flag indicates that the window that received this motion event is partly
* or wholly obscured by another visible window above it. This flag is set to true
* even if the event did not directly pass through the obscured area.
* A security sensitive application can check this flag to identify situations in which
* a malicious application may have covered up part of its content for the purpose
* of misleading the user or hijacking touches. An appropriate response might be
* to drop the suspect touches or to take additional precautions to confirm the user's
* actual intent.
*/
AMOTION_EVENT_FLAG_WINDOW_IS_OBSCURED = 0x1,
};
/**
* Motion event edge touch flags.
*/
enum AndroidMotioneventEdgeTouchTlags
{
/** No edges intersected. */
AMOTION_EVENT_EDGE_FLAG_NONE = 0,
/** Flag indicating the motion event intersected the top edge of the screen. */
AMOTION_EVENT_EDGE_FLAG_TOP = 0x01,
/** Flag indicating the motion event intersected the bottom edge of the screen. */
AMOTION_EVENT_EDGE_FLAG_BOTTOM = 0x02,
/** Flag indicating the motion event intersected the left edge of the screen. */
AMOTION_EVENT_EDGE_FLAG_LEFT = 0x04,
/** Flag indicating the motion event intersected the right edge of the screen. */
AMOTION_EVENT_EDGE_FLAG_RIGHT = 0x08
};
/**
* Constants that identify each individual axis of a motion event.
* @anchor AMOTION_EVENT_AXIS
*/
enum AndroidMotioneventAxis
{
/**
* Axis constant: X axis of a motion event.
*
* - For a touch screen, reports the absolute X screen position of the center of
* the touch contact area. The units are display pixels.
* - For a touch pad, reports the absolute X surface position of the center of the touch
* contact area. The units are device-dependent.
* - For a mouse, reports the absolute X screen position of the mouse pointer.
* The units are display pixels.
* - For a trackball, reports the relative horizontal displacement of the trackball.
* The value is normalized to a range from -1.0 (left) to 1.0 (right).
* - For a joystick, reports the absolute X position of the joystick.
* The value is normalized to a range from -1.0 (left) to 1.0 (right).
*/
AMOTION_EVENT_AXIS_X = 0,
/**
* Axis constant: Y axis of a motion event.
*
* - For a touch screen, reports the absolute Y screen position of the center of
* the touch contact area. The units are display pixels.
* - For a touch pad, reports the absolute Y surface position of the center of the touch
* contact area. The units are device-dependent.
* - For a mouse, reports the absolute Y screen position of the mouse pointer.
* The units are display pixels.
* - For a trackball, reports the relative vertical displacement of the trackball.
* The value is normalized to a range from -1.0 (up) to 1.0 (down).
* - For a joystick, reports the absolute Y position of the joystick.
* The value is normalized to a range from -1.0 (up or far) to 1.0 (down or near).
*/
AMOTION_EVENT_AXIS_Y = 1,
/**
* Axis constant: Pressure axis of a motion event.
*
* - For a touch screen or touch pad, reports the approximate pressure applied to the surface
* by a finger or other tool. The value is normalized to a range from
* 0 (no pressure at all) to 1 (normal pressure), although values higher than 1
* may be generated depending on the calibration of the input device.
* - For a trackball, the value is set to 1 if the trackball button is pressed
* or 0 otherwise.
* - For a mouse, the value is set to 1 if the primary mouse button is pressed
* or 0 otherwise.
*/
AMOTION_EVENT_AXIS_PRESSURE = 2,
/**
* Axis constant: Size axis of a motion event.
*
* - For a touch screen or touch pad, reports the approximate size of the contact area in
* relation to the maximum detectable size for the device. The value is normalized
* to a range from 0 (smallest detectable size) to 1 (largest detectable size),
* although it is not a linear scale. This value is of limited use.
* To obtain calibrated size information, see
* {@link AMOTION_EVENT_AXIS_TOUCH_MAJOR} or {@link AMOTION_EVENT_AXIS_TOOL_MAJOR}.
*/
AMOTION_EVENT_AXIS_SIZE = 3,
/**
* Axis constant: TouchMajor axis of a motion event.
*
* - For a touch screen, reports the length of the major axis of an ellipse that
* represents the touch area at the point of contact.
* The units are display pixels.
* - For a touch pad, reports the length of the major axis of an ellipse that
* represents the touch area at the point of contact.
* The units are device-dependent.
*/
AMOTION_EVENT_AXIS_TOUCH_MAJOR = 4,
/**
* Axis constant: TouchMinor axis of a motion event.
*
* - For a touch screen, reports the length of the minor axis of an ellipse that
* represents the touch area at the point of contact.
* The units are display pixels.
* - For a touch pad, reports the length of the minor axis of an ellipse that
* represents the touch area at the point of contact.
* The units are device-dependent.
*
* When the touch is circular, the major and minor axis lengths will be equal to one another.
*/
AMOTION_EVENT_AXIS_TOUCH_MINOR = 5,
/**
* Axis constant: ToolMajor axis of a motion event.
*
* - For a touch screen, reports the length of the major axis of an ellipse that
* represents the size of the approaching finger or tool used to make contact.
* - For a touch pad, reports the length of the major axis of an ellipse that
* represents the size of the approaching finger or tool used to make contact.
* The units are device-dependent.
*
* When the touch is circular, the major and minor axis lengths will be equal to one another.
*
* The tool size may be larger than the touch size since the tool may not be fully
* in contact with the touch sensor.
*/
AMOTION_EVENT_AXIS_TOOL_MAJOR = 6,
/**
* Axis constant: ToolMinor axis of a motion event.
*
* - For a touch screen, reports the length of the minor axis of an ellipse that
* represents the size of the approaching finger or tool used to make contact.
* - For a touch pad, reports the length of the minor axis of an ellipse that
* represents the size of the approaching finger or tool used to make contact.
* The units are device-dependent.
*
* When the touch is circular, the major and minor axis lengths will be equal to one another.
*
* The tool size may be larger than the touch size since the tool may not be fully
* in contact with the touch sensor.
*/
AMOTION_EVENT_AXIS_TOOL_MINOR = 7,
/**
* Axis constant: Orientation axis of a motion event.
*
* - For a touch screen or touch pad, reports the orientation of the finger
* or tool in radians relative to the vertical plane of the device.
* An angle of 0 radians indicates that the major axis of contact is oriented
* upwards, is perfectly circular or is of unknown orientation. A positive angle
* indicates that the major axis of contact is oriented to the right. A negative angle
* indicates that the major axis of contact is oriented to the left.
* The full range is from -PI/2 radians (finger pointing fully left) to PI/2 radians
* (finger pointing fully right).
* - For a stylus, the orientation indicates the direction in which the stylus
* is pointing in relation to the vertical axis of the current orientation of the screen.
* The range is from -PI radians to PI radians, where 0 is pointing up,
* -PI/2 radians is pointing left, -PI or PI radians is pointing down, and PI/2 radians
* is pointing right. See also {@link AMOTION_EVENT_AXIS_TILT}.
*/
AMOTION_EVENT_AXIS_ORIENTATION = 8,
/**
* Axis constant: Vertical Scroll axis of a motion event.
*
* - For a mouse, reports the relative movement of the vertical scroll wheel.
* The value is normalized to a range from -1.0 (down) to 1.0 (up).
*
* This axis should be used to scroll views vertically.
*/
AMOTION_EVENT_AXIS_VSCROLL = 9,
/**
* Axis constant: Horizontal Scroll axis of a motion event.
*
* - For a mouse, reports the relative movement of the horizontal scroll wheel.
* The value is normalized to a range from -1.0 (left) to 1.0 (right).
*
* This axis should be used to scroll views horizontally.
*/
AMOTION_EVENT_AXIS_HSCROLL = 10,
/**
* Axis constant: Z axis of a motion event.
*
* - For a joystick, reports the absolute Z position of the joystick.
* The value is normalized to a range from -1.0 (high) to 1.0 (low).
* <em>On game pads with two analog joysticks, this axis is often reinterpreted
* to report the absolute X position of the second joystick instead.</em>
*/
AMOTION_EVENT_AXIS_Z = 11,
/**
* Axis constant: X Rotation axis of a motion event.
*
* - For a joystick, reports the absolute rotation angle about the X axis.
* The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise).
*/
AMOTION_EVENT_AXIS_RX = 12,
/**
* Axis constant: Y Rotation axis of a motion event.
*
* - For a joystick, reports the absolute rotation angle about the Y axis.
* The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise).
*/
AMOTION_EVENT_AXIS_RY = 13,
/**
* Axis constant: Z Rotation axis of a motion event.
*
* - For a joystick, reports the absolute rotation angle about the Z axis.
* The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise).
* On game pads with two analog joysticks, this axis is often reinterpreted
* to report the absolute Y position of the second joystick instead.
*/
AMOTION_EVENT_AXIS_RZ = 14,
/**
* Axis constant: Hat X axis of a motion event.
*
* - For a joystick, reports the absolute X position of the directional hat control.
* The value is normalized to a range from -1.0 (left) to 1.0 (right).
*/
AMOTION_EVENT_AXIS_HAT_X = 15,
/**
* Axis constant: Hat Y axis of a motion event.
*
* - For a joystick, reports the absolute Y position of the directional hat control.
* The value is normalized to a range from -1.0 (up) to 1.0 (down).
*/
AMOTION_EVENT_AXIS_HAT_Y = 16,
/**
* Axis constant: Left Trigger axis of a motion event.
*
* - For a joystick, reports the absolute position of the left trigger control.
* The value is normalized to a range from 0.0 (released) to 1.0 (fully pressed).
*/
AMOTION_EVENT_AXIS_LTRIGGER = 17,
/**
* Axis constant: Right Trigger axis of a motion event.
*
* - For a joystick, reports the absolute position of the right trigger control.
* The value is normalized to a range from 0.0 (released) to 1.0 (fully pressed).
*/
AMOTION_EVENT_AXIS_RTRIGGER = 18,
/**
* Axis constant: Throttle axis of a motion event.
*
* - For a joystick, reports the absolute position of the throttle control.
* The value is normalized to a range from 0.0 (fully open) to 1.0 (fully closed).
*/
AMOTION_EVENT_AXIS_THROTTLE = 19,
/**
* Axis constant: Rudder axis of a motion event.
*
* - For a joystick, reports the absolute position of the rudder control.
* The value is normalized to a range from -1.0 (turn left) to 1.0 (turn right).
*/
AMOTION_EVENT_AXIS_RUDDER = 20,
/**
* Axis constant: Wheel axis of a motion event.
*
* - For a joystick, reports the absolute position of the steering wheel control.
* The value is normalized to a range from -1.0 (turn left) to 1.0 (turn right).
*/
AMOTION_EVENT_AXIS_WHEEL = 21,
/**
* Axis constant: Gas axis of a motion event.
*
* - For a joystick, reports the absolute position of the gas (accelerator) control.
* The value is normalized to a range from 0.0 (no acceleration)
* to 1.0 (maximum acceleration).
*/
AMOTION_EVENT_AXIS_GAS = 22,
/**
* Axis constant: Brake axis of a motion event.
*
* - For a joystick, reports the absolute position of the brake control.
* The value is normalized to a range from 0.0 (no braking) to 1.0 (maximum braking).
*/
AMOTION_EVENT_AXIS_BRAKE = 23,
/**
* Axis constant: Distance axis of a motion event.
*
* - For a stylus, reports the distance of the stylus from the screen.
* A value of 0.0 indicates direct contact and larger values indicate increasing
* distance from the surface.
*/
AMOTION_EVENT_AXIS_DISTANCE = 24,
/**
* Axis constant: Tilt axis of a motion event.
*
* - For a stylus, reports the tilt angle of the stylus in radians where
* 0 radians indicates that the stylus is being held perpendicular to the
* surface, and PI/2 radians indicates that the stylus is being held flat
* against the surface.
*/
AMOTION_EVENT_AXIS_TILT = 25,
/**
* Axis constant: Generic scroll axis of a motion event.
*
* - This is used for scroll axis motion events that can't be classified as strictly
* vertical or horizontal. The movement of a rotating scroller is an example of this.
*/
AMOTION_EVENT_AXIS_SCROLL = 26,
/**
* Axis constant: The movement of x position of a motion event.
*
* - For a mouse, reports a difference of x position between the previous position.
* This is useful when pointer is captured, in that case the mouse pointer doesn't
* change the location but this axis reports the difference which allows the app
* to see how the mouse is moved.
*/
AMOTION_EVENT_AXIS_RELATIVE_X = 27,
/**
* Axis constant: The movement of y position of a motion event.
*
* Same as {@link RELATIVE_X}, but for y position.
*/
AMOTION_EVENT_AXIS_RELATIVE_Y = 28,
/**
* Axis constant: Generic 1 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_1 = 32,
/**
* Axis constant: Generic 2 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_2 = 33,
/**
* Axis constant: Generic 3 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_3 = 34,
/**
* Axis constant: Generic 4 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_4 = 35,
/**
* Axis constant: Generic 5 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_5 = 36,
/**
* Axis constant: Generic 6 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_6 = 37,
/**
* Axis constant: Generic 7 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_7 = 38,
/**
* Axis constant: Generic 8 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_8 = 39,
/**
* Axis constant: Generic 9 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_9 = 40,
/**
* Axis constant: Generic 10 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_10 = 41,
/**
* Axis constant: Generic 11 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_11 = 42,
/**
* Axis constant: Generic 12 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_12 = 43,
/**
* Axis constant: Generic 13 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_13 = 44,
/**
* Axis constant: Generic 14 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_14 = 45,
/**
* Axis constant: Generic 15 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_15 = 46,
/**
* Axis constant: Generic 16 axis of a motion event.
* The interpretation of a generic axis is device-specific.
*/
AMOTION_EVENT_AXIS_GENERIC_16 = 47,
// NOTE: If you add a new axis here you must also add it to several other files.
// Refer to frameworks/base/core/java/android/view/MotionEvent.java for the full list.
};
/**
* Constants that identify buttons that are associated with motion events.
* Refer to the documentation on the MotionEvent class for descriptions of each button.
*/
enum AndroidMotioneventButtons
{
/** primary */
AMOTION_EVENT_BUTTON_PRIMARY = 1 << 0,
/** secondary */
AMOTION_EVENT_BUTTON_SECONDARY = 1 << 1,
/** tertiary */
AMOTION_EVENT_BUTTON_TERTIARY = 1 << 2,
/** back */
AMOTION_EVENT_BUTTON_BACK = 1 << 3,
/** forward */
AMOTION_EVENT_BUTTON_FORWARD = 1 << 4,
AMOTION_EVENT_BUTTON_STYLUS_PRIMARY = 1 << 5,
AMOTION_EVENT_BUTTON_STYLUS_SECONDARY = 1 << 6,
};
/**
* Constants that identify tool types.
* Refer to the documentation on the MotionEvent class for descriptions of each tool type.
*/
enum AndroidMotioneventToolType
{
/** unknown */
AMOTION_EVENT_TOOL_TYPE_UNKNOWN = 0,
/** finger */
AMOTION_EVENT_TOOL_TYPE_FINGER = 1,
/** stylus */
AMOTION_EVENT_TOOL_TYPE_STYLUS = 2,
/** mouse */
AMOTION_EVENT_TOOL_TYPE_MOUSE = 3,
/** eraser */
AMOTION_EVENT_TOOL_TYPE_ERASER = 4,
};
/**
* Input source masks.
*
* Refer to the documentation on android.view.InputDevice for more details about input sources
* and their correct interpretation.
*/
enum AndroidInputSourceClass
{
/** mask */
AINPUT_SOURCE_CLASS_MASK = 0x000000ff,
/** none */
AINPUT_SOURCE_CLASS_NONE = 0x00000000,
/** button */
AINPUT_SOURCE_CLASS_BUTTON = 0x00000001,
/** pointer */
AINPUT_SOURCE_CLASS_POINTER = 0x00000002,
/** navigation */
AINPUT_SOURCE_CLASS_NAVIGATION = 0x00000004,
/** position */
AINPUT_SOURCE_CLASS_POSITION = 0x00000008,
/** joystick */
AINPUT_SOURCE_CLASS_JOYSTICK = 0x00000010,
};
/**
* Input sources.
*/
enum AndroidInputSource
{
/** unknown */
AINPUT_SOURCE_UNKNOWN = 0x00000000,
/** keyboard */
AINPUT_SOURCE_KEYBOARD = 0x00000100 | AINPUT_SOURCE_CLASS_BUTTON,
/** dpad */
AINPUT_SOURCE_DPAD = 0x00000200 | AINPUT_SOURCE_CLASS_BUTTON,
/** gamepad */
AINPUT_SOURCE_GAMEPAD = 0x00000400 | AINPUT_SOURCE_CLASS_BUTTON,
/** touchscreen */
AINPUT_SOURCE_TOUCHSCREEN = 0x00001000 | AINPUT_SOURCE_CLASS_POINTER,
/** mouse */
AINPUT_SOURCE_MOUSE = 0x00002000 | AINPUT_SOURCE_CLASS_POINTER,
/** stylus */
AINPUT_SOURCE_STYLUS = 0x00004000 | AINPUT_SOURCE_CLASS_POINTER,
/** bluetooth stylus */
AINPUT_SOURCE_BLUETOOTH_STYLUS = 0x00008000 | AINPUT_SOURCE_STYLUS,
/** trackball */
AINPUT_SOURCE_TRACKBALL = 0x00010000 | AINPUT_SOURCE_CLASS_NAVIGATION,
/** mouse relative */
AINPUT_SOURCE_MOUSE_RELATIVE = 0x00020000 | AINPUT_SOURCE_CLASS_NAVIGATION,
/** touchpad */
AINPUT_SOURCE_TOUCHPAD = 0x00100000 | AINPUT_SOURCE_CLASS_POSITION,
/** navigation */
AINPUT_SOURCE_TOUCH_NAVIGATION = 0x00200000 | AINPUT_SOURCE_CLASS_NONE,
/** joystick */
AINPUT_SOURCE_JOYSTICK = 0x01000000 | AINPUT_SOURCE_CLASS_JOYSTICK,
/** rotary encoder */
AINPUT_SOURCE_ROTARY_ENCODER = 0x00400000 | AINPUT_SOURCE_CLASS_NONE,
};
/**
* Keyboard types.
*
* Refer to the documentation on android.view.InputDevice for more details.
*/
enum AndroidKeyboardType
{
/** none */
AINPUT_KEYBOARD_TYPE_NONE = 0,
/** non alphabetic */
AINPUT_KEYBOARD_TYPE_NON_ALPHABETIC = 1,
/** alphabetic */
AINPUT_KEYBOARD_TYPE_ALPHABETIC = 2,
};
/**
* Constants used to retrieve information about the range of motion for a particular
* coordinate of a motion event.
*
* Refer to the documentation on android.view.InputDevice for more details about input sources
* and their correct interpretation.
*
* @deprecated These constants are deprecated. Use {@link AMOTION_EVENT_AXIS AMOTION_EVENT_AXIS_*} constants instead.
*/
enum AndroidMotionRange
{
/** x */
AINPUT_MOTION_RANGE_X = AMOTION_EVENT_AXIS_X,
/** y */
AINPUT_MOTION_RANGE_Y = AMOTION_EVENT_AXIS_Y,
/** pressure */
AINPUT_MOTION_RANGE_PRESSURE = AMOTION_EVENT_AXIS_PRESSURE,
/** size */
AINPUT_MOTION_RANGE_SIZE = AMOTION_EVENT_AXIS_SIZE,
/** touch major */
AINPUT_MOTION_RANGE_TOUCH_MAJOR = AMOTION_EVENT_AXIS_TOUCH_MAJOR,
/** touch minor */
AINPUT_MOTION_RANGE_TOUCH_MINOR = AMOTION_EVENT_AXIS_TOUCH_MINOR,
/** tool major */
AINPUT_MOTION_RANGE_TOOL_MAJOR = AMOTION_EVENT_AXIS_TOOL_MAJOR,
/** tool minor */
AINPUT_MOTION_RANGE_TOOL_MINOR = AMOTION_EVENT_AXIS_TOOL_MINOR,
/** orientation */
AINPUT_MOTION_RANGE_ORIENTATION = AMOTION_EVENT_AXIS_ORIENTATION,
};
#endif // _ANDROID_INPUT_H

View file

@ -0,0 +1,746 @@
// copied from <https://android.googlesource.com/platform/frameworks/native/+/master/include/android/keycodes.h>
// blob 2164d6163e1646c22825e364cad4f3c47638effd
// (and modified)
/*
* Copyright (C) 2010 The Android Open Source Project
*
* 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.
*/
#ifndef _ANDROID_KEYCODES_H
#define _ANDROID_KEYCODES_H
/**
* Key codes.
*/
enum AndroidKeycode
{
/** Unknown key code. */
AKEYCODE_UNKNOWN = 0,
/** Soft Left key.
* Usually situated below the display on phones and used as a multi-function
* feature key for selecting a software defined function shown on the bottom left
* of the display. */
AKEYCODE_SOFT_LEFT = 1,
/** Soft Right key.
* Usually situated below the display on phones and used as a multi-function
* feature key for selecting a software defined function shown on the bottom right
* of the display. */
AKEYCODE_SOFT_RIGHT = 2,
/** Home key.
* This key is handled by the framework and is never delivered to applications. */
AKEYCODE_HOME = 3,
/** Back key. */
AKEYCODE_BACK = 4,
/** Call key. */
AKEYCODE_CALL = 5,
/** End Call key. */
AKEYCODE_ENDCALL = 6,
/** '0' key. */
AKEYCODE_0 = 7,
/** '1' key. */
AKEYCODE_1 = 8,
/** '2' key. */
AKEYCODE_2 = 9,
/** '3' key. */
AKEYCODE_3 = 10,
/** '4' key. */
AKEYCODE_4 = 11,
/** '5' key. */
AKEYCODE_5 = 12,
/** '6' key. */
AKEYCODE_6 = 13,
/** '7' key. */
AKEYCODE_7 = 14,
/** '8' key. */
AKEYCODE_8 = 15,
/** '9' key. */
AKEYCODE_9 = 16,
/** '*' key. */
AKEYCODE_STAR = 17,
/** '#' key. */
AKEYCODE_POUND = 18,
/** Directional Pad Up key.
* May also be synthesized from trackball motions. */
AKEYCODE_DPAD_UP = 19,
/** Directional Pad Down key.
* May also be synthesized from trackball motions. */
AKEYCODE_DPAD_DOWN = 20,
/** Directional Pad Left key.
* May also be synthesized from trackball motions. */
AKEYCODE_DPAD_LEFT = 21,
/** Directional Pad Right key.
* May also be synthesized from trackball motions. */
AKEYCODE_DPAD_RIGHT = 22,
/** Directional Pad Center key.
* May also be synthesized from trackball motions. */
AKEYCODE_DPAD_CENTER = 23,
/** Volume Up key.
* Adjusts the speaker volume up. */
AKEYCODE_VOLUME_UP = 24,
/** Volume Down key.
* Adjusts the speaker volume down. */
AKEYCODE_VOLUME_DOWN = 25,
/** Power key. */
AKEYCODE_POWER = 26,
/** Camera key.
* Used to launch a camera application or take pictures. */
AKEYCODE_CAMERA = 27,
/** Clear key. */
AKEYCODE_CLEAR = 28,
/** 'A' key. */
AKEYCODE_A = 29,
/** 'B' key. */
AKEYCODE_B = 30,
/** 'C' key. */
AKEYCODE_C = 31,
/** 'D' key. */
AKEYCODE_D = 32,
/** 'E' key. */
AKEYCODE_E = 33,
/** 'F' key. */
AKEYCODE_F = 34,
/** 'G' key. */
AKEYCODE_G = 35,
/** 'H' key. */
AKEYCODE_H = 36,
/** 'I' key. */
AKEYCODE_I = 37,
/** 'J' key. */
AKEYCODE_J = 38,
/** 'K' key. */
AKEYCODE_K = 39,
/** 'L' key. */
AKEYCODE_L = 40,
/** 'M' key. */
AKEYCODE_M = 41,
/** 'N' key. */
AKEYCODE_N = 42,
/** 'O' key. */
AKEYCODE_O = 43,
/** 'P' key. */
AKEYCODE_P = 44,
/** 'Q' key. */
AKEYCODE_Q = 45,
/** 'R' key. */
AKEYCODE_R = 46,
/** 'S' key. */
AKEYCODE_S = 47,
/** 'T' key. */
AKEYCODE_T = 48,
/** 'U' key. */
AKEYCODE_U = 49,
/** 'V' key. */
AKEYCODE_V = 50,
/** 'W' key. */
AKEYCODE_W = 51,
/** 'X' key. */
AKEYCODE_X = 52,
/** 'Y' key. */
AKEYCODE_Y = 53,
/** 'Z' key. */
AKEYCODE_Z = 54,
/** ',' key. */
AKEYCODE_COMMA = 55,
/** '.' key. */
AKEYCODE_PERIOD = 56,
/** Left Alt modifier key. */
AKEYCODE_ALT_LEFT = 57,
/** Right Alt modifier key. */
AKEYCODE_ALT_RIGHT = 58,
/** Left Shift modifier key. */
AKEYCODE_SHIFT_LEFT = 59,
/** Right Shift modifier key. */
AKEYCODE_SHIFT_RIGHT = 60,
/** Tab key. */
AKEYCODE_TAB = 61,
/** Space key. */
AKEYCODE_SPACE = 62,
/** Symbol modifier key.
* Used to enter alternate symbols. */
AKEYCODE_SYM = 63,
/** Explorer special function key.
* Used to launch a browser application. */
AKEYCODE_EXPLORER = 64,
/** Envelope special function key.
* Used to launch a mail application. */
AKEYCODE_ENVELOPE = 65,
/** Enter key. */
AKEYCODE_ENTER = 66,
/** Backspace key.
* Deletes characters before the insertion point, unlike {@link AKEYCODE_FORWARD_DEL}. */
AKEYCODE_DEL = 67,
/** '`' (backtick) key. */
AKEYCODE_GRAVE = 68,
/** '-'. */
AKEYCODE_MINUS = 69,
/** '=' key. */
AKEYCODE_EQUALS = 70,
/** '[' key. */
AKEYCODE_LEFT_BRACKET = 71,
/** ']' key. */
AKEYCODE_RIGHT_BRACKET = 72,
/** '\' key. */
AKEYCODE_BACKSLASH = 73,
/** ';' key. */
AKEYCODE_SEMICOLON = 74,
/** ''' (apostrophe) key. */
AKEYCODE_APOSTROPHE = 75,
/** '/' key. */
AKEYCODE_SLASH = 76,
/** '@' key. */
AKEYCODE_AT = 77,
/** Number modifier key.
* Used to enter numeric symbols.
* This key is not {@link AKEYCODE_NUM_LOCK}; it is more like {@link AKEYCODE_ALT_LEFT}. */
AKEYCODE_NUM = 78,
/** Headset Hook key.
* Used to hang up calls and stop media. */
AKEYCODE_HEADSETHOOK = 79,
/** Camera Focus key.
* Used to focus the camera. */
AKEYCODE_FOCUS = 80,
/** '+' key. */
AKEYCODE_PLUS = 81,
/** Menu key. */
AKEYCODE_MENU = 82,
/** Notification key. */
AKEYCODE_NOTIFICATION = 83,
/** Search key. */
AKEYCODE_SEARCH = 84,
/** Play/Pause media key. */
AKEYCODE_MEDIA_PLAY_PAUSE = 85,
/** Stop media key. */
AKEYCODE_MEDIA_STOP = 86,
/** Play Next media key. */
AKEYCODE_MEDIA_NEXT = 87,
/** Play Previous media key. */
AKEYCODE_MEDIA_PREVIOUS = 88,
/** Rewind media key. */
AKEYCODE_MEDIA_REWIND = 89,
/** Fast Forward media key. */
AKEYCODE_MEDIA_FAST_FORWARD = 90,
/** Mute key.
* Mutes the microphone, unlike {@link AKEYCODE_VOLUME_MUTE}. */
AKEYCODE_MUTE = 91,
/** Page Up key. */
AKEYCODE_PAGE_UP = 92,
/** Page Down key. */
AKEYCODE_PAGE_DOWN = 93,
/** Picture Symbols modifier key.
* Used to switch symbol sets (Emoji, Kao-moji). */
AKEYCODE_PICTSYMBOLS = 94,
/** Switch Charset modifier key.
* Used to switch character sets (Kanji, Katakana). */
AKEYCODE_SWITCH_CHARSET = 95,
/** A Button key.
* On a game controller, the A button should be either the button labeled A
* or the first button on the bottom row of controller buttons. */
AKEYCODE_BUTTON_A = 96,
/** B Button key.
* On a game controller, the B button should be either the button labeled B
* or the second button on the bottom row of controller buttons. */
AKEYCODE_BUTTON_B = 97,
/** C Button key.
* On a game controller, the C button should be either the button labeled C
* or the third button on the bottom row of controller buttons. */
AKEYCODE_BUTTON_C = 98,
/** X Button key.
* On a game controller, the X button should be either the button labeled X
* or the first button on the upper row of controller buttons. */
AKEYCODE_BUTTON_X = 99,
/** Y Button key.
* On a game controller, the Y button should be either the button labeled Y
* or the second button on the upper row of controller buttons. */
AKEYCODE_BUTTON_Y = 100,
/** Z Button key.
* On a game controller, the Z button should be either the button labeled Z
* or the third button on the upper row of controller buttons. */
AKEYCODE_BUTTON_Z = 101,
/** L1 Button key.
* On a game controller, the L1 button should be either the button labeled L1 (or L)
* or the top left trigger button. */
AKEYCODE_BUTTON_L1 = 102,
/** R1 Button key.
* On a game controller, the R1 button should be either the button labeled R1 (or R)
* or the top right trigger button. */
AKEYCODE_BUTTON_R1 = 103,
/** L2 Button key.
* On a game controller, the L2 button should be either the button labeled L2
* or the bottom left trigger button. */
AKEYCODE_BUTTON_L2 = 104,
/** R2 Button key.
* On a game controller, the R2 button should be either the button labeled R2
* or the bottom right trigger button. */
AKEYCODE_BUTTON_R2 = 105,
/** Left Thumb Button key.
* On a game controller, the left thumb button indicates that the left (or only)
* joystick is pressed. */
AKEYCODE_BUTTON_THUMBL = 106,
/** Right Thumb Button key.
* On a game controller, the right thumb button indicates that the right
* joystick is pressed. */
AKEYCODE_BUTTON_THUMBR = 107,
/** Start Button key.
* On a game controller, the button labeled Start. */
AKEYCODE_BUTTON_START = 108,
/** Select Button key.
* On a game controller, the button labeled Select. */
AKEYCODE_BUTTON_SELECT = 109,
/** Mode Button key.
* On a game controller, the button labeled Mode. */
AKEYCODE_BUTTON_MODE = 110,
/** Escape key. */
AKEYCODE_ESCAPE = 111,
/** Forward Delete key.
* Deletes characters ahead of the insertion point, unlike {@link AKEYCODE_DEL}. */
AKEYCODE_FORWARD_DEL = 112,
/** Left Control modifier key. */
AKEYCODE_CTRL_LEFT = 113,
/** Right Control modifier key. */
AKEYCODE_CTRL_RIGHT = 114,
/** Caps Lock key. */
AKEYCODE_CAPS_LOCK = 115,
/** Scroll Lock key. */
AKEYCODE_SCROLL_LOCK = 116,
/** Left Meta modifier key. */
AKEYCODE_META_LEFT = 117,
/** Right Meta modifier key. */
AKEYCODE_META_RIGHT = 118,
/** Function modifier key. */
AKEYCODE_FUNCTION = 119,
/** System Request / Print Screen key. */
AKEYCODE_SYSRQ = 120,
/** Break / Pause key. */
AKEYCODE_BREAK = 121,
/** Home Movement key.
* Used for scrolling or moving the cursor around to the start of a line
* or to the top of a list. */
AKEYCODE_MOVE_HOME = 122,
/** End Movement key.
* Used for scrolling or moving the cursor around to the end of a line
* or to the bottom of a list. */
AKEYCODE_MOVE_END = 123,
/** Insert key.
* Toggles insert / overwrite edit mode. */
AKEYCODE_INSERT = 124,
/** Forward key.
* Navigates forward in the history stack. Complement of {@link AKEYCODE_BACK}. */
AKEYCODE_FORWARD = 125,
/** Play media key. */
AKEYCODE_MEDIA_PLAY = 126,
/** Pause media key. */
AKEYCODE_MEDIA_PAUSE = 127,
/** Close media key.
* May be used to close a CD tray, for example. */
AKEYCODE_MEDIA_CLOSE = 128,
/** Eject media key.
* May be used to eject a CD tray, for example. */
AKEYCODE_MEDIA_EJECT = 129,
/** Record media key. */
AKEYCODE_MEDIA_RECORD = 130,
/** F1 key. */
AKEYCODE_F1 = 131,
/** F2 key. */
AKEYCODE_F2 = 132,
/** F3 key. */
AKEYCODE_F3 = 133,
/** F4 key. */
AKEYCODE_F4 = 134,
/** F5 key. */
AKEYCODE_F5 = 135,
/** F6 key. */
AKEYCODE_F6 = 136,
/** F7 key. */
AKEYCODE_F7 = 137,
/** F8 key. */
AKEYCODE_F8 = 138,
/** F9 key. */
AKEYCODE_F9 = 139,
/** F10 key. */
AKEYCODE_F10 = 140,
/** F11 key. */
AKEYCODE_F11 = 141,
/** F12 key. */
AKEYCODE_F12 = 142,
/** Num Lock key.
* This is the Num Lock key; it is different from {@link AKEYCODE_NUM}.
* This key alters the behavior of other keys on the numeric keypad. */
AKEYCODE_NUM_LOCK = 143,
/** Numeric keypad '0' key. */
AKEYCODE_NUMPAD_0 = 144,
/** Numeric keypad '1' key. */
AKEYCODE_NUMPAD_1 = 145,
/** Numeric keypad '2' key. */
AKEYCODE_NUMPAD_2 = 146,
/** Numeric keypad '3' key. */
AKEYCODE_NUMPAD_3 = 147,
/** Numeric keypad '4' key. */
AKEYCODE_NUMPAD_4 = 148,
/** Numeric keypad '5' key. */
AKEYCODE_NUMPAD_5 = 149,
/** Numeric keypad '6' key. */
AKEYCODE_NUMPAD_6 = 150,
/** Numeric keypad '7' key. */
AKEYCODE_NUMPAD_7 = 151,
/** Numeric keypad '8' key. */
AKEYCODE_NUMPAD_8 = 152,
/** Numeric keypad '9' key. */
AKEYCODE_NUMPAD_9 = 153,
/** Numeric keypad '/' key (for division). */
AKEYCODE_NUMPAD_DIVIDE = 154,
/** Numeric keypad '*' key (for multiplication). */
AKEYCODE_NUMPAD_MULTIPLY = 155,
/** Numeric keypad '-' key (for subtraction). */
AKEYCODE_NUMPAD_SUBTRACT = 156,
/** Numeric keypad '+' key (for addition). */
AKEYCODE_NUMPAD_ADD = 157,
/** Numeric keypad '.' key (for decimals or digit grouping). */
AKEYCODE_NUMPAD_DOT = 158,
/** Numeric keypad ',' key (for decimals or digit grouping). */
AKEYCODE_NUMPAD_COMMA = 159,
/** Numeric keypad Enter key. */
AKEYCODE_NUMPAD_ENTER = 160,
/** Numeric keypad '=' key. */
AKEYCODE_NUMPAD_EQUALS = 161,
/** Numeric keypad '(' key. */
AKEYCODE_NUMPAD_LEFT_PAREN = 162,
/** Numeric keypad ')' key. */
AKEYCODE_NUMPAD_RIGHT_PAREN = 163,
/** Volume Mute key.
* Mutes the speaker, unlike {@link AKEYCODE_MUTE}.
* This key should normally be implemented as a toggle such that the first press
* mutes the speaker and the second press restores the original volume. */
AKEYCODE_VOLUME_MUTE = 164,
/** Info key.
* Common on TV remotes to show additional information related to what is
* currently being viewed. */
AKEYCODE_INFO = 165,
/** Channel up key.
* On TV remotes, increments the television channel. */
AKEYCODE_CHANNEL_UP = 166,
/** Channel down key.
* On TV remotes, decrements the television channel. */
AKEYCODE_CHANNEL_DOWN = 167,
/** Zoom in key. */
AKEYCODE_ZOOM_IN = 168,
/** Zoom out key. */
AKEYCODE_ZOOM_OUT = 169,
/** TV key.
* On TV remotes, switches to viewing live TV. */
AKEYCODE_TV = 170,
/** Window key.
* On TV remotes, toggles picture-in-picture mode or other windowing functions. */
AKEYCODE_WINDOW = 171,
/** Guide key.
* On TV remotes, shows a programming guide. */
AKEYCODE_GUIDE = 172,
/** DVR key.
* On some TV remotes, switches to a DVR mode for recorded shows. */
AKEYCODE_DVR = 173,
/** Bookmark key.
* On some TV remotes, bookmarks content or web pages. */
AKEYCODE_BOOKMARK = 174,
/** Toggle captions key.
* Switches the mode for closed-captioning text, for example during television shows. */
AKEYCODE_CAPTIONS = 175,
/** Settings key.
* Starts the system settings activity. */
AKEYCODE_SETTINGS = 176,
/** TV power key.
* On TV remotes, toggles the power on a television screen. */
AKEYCODE_TV_POWER = 177,
/** TV input key.
* On TV remotes, switches the input on a television screen. */
AKEYCODE_TV_INPUT = 178,
/** Set-top-box power key.
* On TV remotes, toggles the power on an external Set-top-box. */
AKEYCODE_STB_POWER = 179,
/** Set-top-box input key.
* On TV remotes, switches the input mode on an external Set-top-box. */
AKEYCODE_STB_INPUT = 180,
/** A/V Receiver power key.
* On TV remotes, toggles the power on an external A/V Receiver. */
AKEYCODE_AVR_POWER = 181,
/** A/V Receiver input key.
* On TV remotes, switches the input mode on an external A/V Receiver. */
AKEYCODE_AVR_INPUT = 182,
/** Red "programmable" key.
* On TV remotes, acts as a contextual/programmable key. */
AKEYCODE_PROG_RED = 183,
/** Green "programmable" key.
* On TV remotes, actsas a contextual/programmable key. */
AKEYCODE_PROG_GREEN = 184,
/** Yellow "programmable" key.
* On TV remotes, acts as a contextual/programmable key. */
AKEYCODE_PROG_YELLOW = 185,
/** Blue "programmable" key.
* On TV remotes, acts as a contextual/programmable key. */
AKEYCODE_PROG_BLUE = 186,
/** App switch key.
* Should bring up the application switcher dialog. */
AKEYCODE_APP_SWITCH = 187,
/** Generic Game Pad Button #1.*/
AKEYCODE_BUTTON_1 = 188,
/** Generic Game Pad Button #2.*/
AKEYCODE_BUTTON_2 = 189,
/** Generic Game Pad Button #3.*/
AKEYCODE_BUTTON_3 = 190,
/** Generic Game Pad Button #4.*/
AKEYCODE_BUTTON_4 = 191,
/** Generic Game Pad Button #5.*/
AKEYCODE_BUTTON_5 = 192,
/** Generic Game Pad Button #6.*/
AKEYCODE_BUTTON_6 = 193,
/** Generic Game Pad Button #7.*/
AKEYCODE_BUTTON_7 = 194,
/** Generic Game Pad Button #8.*/
AKEYCODE_BUTTON_8 = 195,
/** Generic Game Pad Button #9.*/
AKEYCODE_BUTTON_9 = 196,
/** Generic Game Pad Button #10.*/
AKEYCODE_BUTTON_10 = 197,
/** Generic Game Pad Button #11.*/
AKEYCODE_BUTTON_11 = 198,
/** Generic Game Pad Button #12.*/
AKEYCODE_BUTTON_12 = 199,
/** Generic Game Pad Button #13.*/
AKEYCODE_BUTTON_13 = 200,
/** Generic Game Pad Button #14.*/
AKEYCODE_BUTTON_14 = 201,
/** Generic Game Pad Button #15.*/
AKEYCODE_BUTTON_15 = 202,
/** Generic Game Pad Button #16.*/
AKEYCODE_BUTTON_16 = 203,
/** Language Switch key.
* Toggles the current input language such as switching between English and Japanese on
* a QWERTY keyboard. On some devices, the same function may be performed by
* pressing Shift+Spacebar. */
AKEYCODE_LANGUAGE_SWITCH = 204,
/** Manner Mode key.
* Toggles silent or vibrate mode on and off to make the device behave more politely
* in certain settings such as on a crowded train. On some devices, the key may only
* operate when long-pressed. */
AKEYCODE_MANNER_MODE = 205,
/** 3D Mode key.
* Toggles the display between 2D and 3D mode. */
AKEYCODE_3D_MODE = 206,
/** Contacts special function key.
* Used to launch an address book application. */
AKEYCODE_CONTACTS = 207,
/** Calendar special function key.
* Used to launch a calendar application. */
AKEYCODE_CALENDAR = 208,
/** Music special function key.
* Used to launch a music player application. */
AKEYCODE_MUSIC = 209,
/** Calculator special function key.
* Used to launch a calculator application. */
AKEYCODE_CALCULATOR = 210,
/** Japanese full-width / half-width key. */
AKEYCODE_ZENKAKU_HANKAKU = 211,
/** Japanese alphanumeric key. */
AKEYCODE_EISU = 212,
/** Japanese non-conversion key. */
AKEYCODE_MUHENKAN = 213,
/** Japanese conversion key. */
AKEYCODE_HENKAN = 214,
/** Japanese katakana / hiragana key. */
AKEYCODE_KATAKANA_HIRAGANA = 215,
/** Japanese Yen key. */
AKEYCODE_YEN = 216,
/** Japanese Ro key. */
AKEYCODE_RO = 217,
/** Japanese kana key. */
AKEYCODE_KANA = 218,
/** Assist key.
* Launches the global assist activity. Not delivered to applications. */
AKEYCODE_ASSIST = 219,
/** Brightness Down key.
* Adjusts the screen brightness down. */
AKEYCODE_BRIGHTNESS_DOWN = 220,
/** Brightness Up key.
* Adjusts the screen brightness up. */
AKEYCODE_BRIGHTNESS_UP = 221,
/** Audio Track key.
* Switches the audio tracks. */
AKEYCODE_MEDIA_AUDIO_TRACK = 222,
/** Sleep key.
* Puts the device to sleep. Behaves somewhat like {@link AKEYCODE_POWER} but it
* has no effect if the device is already asleep. */
AKEYCODE_SLEEP = 223,
/** Wakeup key.
* Wakes up the device. Behaves somewhat like {@link AKEYCODE_POWER} but it
* has no effect if the device is already awake. */
AKEYCODE_WAKEUP = 224,
/** Pairing key.
* Initiates peripheral pairing mode. Useful for pairing remote control
* devices or game controllers, especially if no other input mode is
* available. */
AKEYCODE_PAIRING = 225,
/** Media Top Menu key.
* Goes to the top of media menu. */
AKEYCODE_MEDIA_TOP_MENU = 226,
/** '11' key. */
AKEYCODE_11 = 227,
/** '12' key. */
AKEYCODE_12 = 228,
/** Last Channel key.
* Goes to the last viewed channel. */
AKEYCODE_LAST_CHANNEL = 229,
/** TV data service key.
* Displays data services like weather, sports. */
AKEYCODE_TV_DATA_SERVICE = 230,
/** Voice Assist key.
* Launches the global voice assist activity. Not delivered to applications. */
AKEYCODE_VOICE_ASSIST = 231,
/** Radio key.
* Toggles TV service / Radio service. */
AKEYCODE_TV_RADIO_SERVICE = 232,
/** Teletext key.
* Displays Teletext service. */
AKEYCODE_TV_TELETEXT = 233,
/** Number entry key.
* Initiates to enter multi-digit channel nubmber when each digit key is assigned
* for selecting separate channel. Corresponds to Number Entry Mode (0x1D) of CEC
* User Control Code. */
AKEYCODE_TV_NUMBER_ENTRY = 234,
/** Analog Terrestrial key.
* Switches to analog terrestrial broadcast service. */
AKEYCODE_TV_TERRESTRIAL_ANALOG = 235,
/** Digital Terrestrial key.
* Switches to digital terrestrial broadcast service. */
AKEYCODE_TV_TERRESTRIAL_DIGITAL = 236,
/** Satellite key.
* Switches to digital satellite broadcast service. */
AKEYCODE_TV_SATELLITE = 237,
/** BS key.
* Switches to BS digital satellite broadcasting service available in Japan. */
AKEYCODE_TV_SATELLITE_BS = 238,
/** CS key.
* Switches to CS digital satellite broadcasting service available in Japan. */
AKEYCODE_TV_SATELLITE_CS = 239,
/** BS/CS key.
* Toggles between BS and CS digital satellite services. */
AKEYCODE_TV_SATELLITE_SERVICE = 240,
/** Toggle Network key.
* Toggles selecting broacast services. */
AKEYCODE_TV_NETWORK = 241,
/** Antenna/Cable key.
* Toggles broadcast input source between antenna and cable. */
AKEYCODE_TV_ANTENNA_CABLE = 242,
/** HDMI #1 key.
* Switches to HDMI input #1. */
AKEYCODE_TV_INPUT_HDMI_1 = 243,
/** HDMI #2 key.
* Switches to HDMI input #2. */
AKEYCODE_TV_INPUT_HDMI_2 = 244,
/** HDMI #3 key.
* Switches to HDMI input #3. */
AKEYCODE_TV_INPUT_HDMI_3 = 245,
/** HDMI #4 key.
* Switches to HDMI input #4. */
AKEYCODE_TV_INPUT_HDMI_4 = 246,
/** Composite #1 key.
* Switches to composite video input #1. */
AKEYCODE_TV_INPUT_COMPOSITE_1 = 247,
/** Composite #2 key.
* Switches to composite video input #2. */
AKEYCODE_TV_INPUT_COMPOSITE_2 = 248,
/** Component #1 key.
* Switches to component video input #1. */
AKEYCODE_TV_INPUT_COMPONENT_1 = 249,
/** Component #2 key.
* Switches to component video input #2. */
AKEYCODE_TV_INPUT_COMPONENT_2 = 250,
/** VGA #1 key.
* Switches to VGA (analog RGB) input #1. */
AKEYCODE_TV_INPUT_VGA_1 = 251,
/** Audio description key.
* Toggles audio description off / on. */
AKEYCODE_TV_AUDIO_DESCRIPTION = 252,
/** Audio description mixing volume up key.
* Louden audio description volume as compared with normal audio volume. */
AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP = 253,
/** Audio description mixing volume down key.
* Lessen audio description volume as compared with normal audio volume. */
AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN = 254,
/** Zoom mode key.
* Changes Zoom mode (Normal, Full, Zoom, Wide-zoom, etc.) */
AKEYCODE_TV_ZOOM_MODE = 255,
/** Contents menu key.
* Goes to the title list. Corresponds to Contents Menu (0x0B) of CEC User Control
* Code */
AKEYCODE_TV_CONTENTS_MENU = 256,
/** Media context menu key.
* Goes to the context menu of media contents. Corresponds to Media Context-sensitive
* Menu (0x11) of CEC User Control Code. */
AKEYCODE_TV_MEDIA_CONTEXT_MENU = 257,
/** Timer programming key.
* Goes to the timer recording menu. Corresponds to Timer Programming (0x54) of
* CEC User Control Code. */
AKEYCODE_TV_TIMER_PROGRAMMING = 258,
/** Help key. */
AKEYCODE_HELP = 259,
AKEYCODE_NAVIGATE_PREVIOUS = 260,
AKEYCODE_NAVIGATE_NEXT = 261,
AKEYCODE_NAVIGATE_IN = 262,
AKEYCODE_NAVIGATE_OUT = 263,
/** Primary stem key for Wear
* Main power/reset button on watch. */
AKEYCODE_STEM_PRIMARY = 264,
/** Generic stem key 1 for Wear */
AKEYCODE_STEM_1 = 265,
/** Generic stem key 2 for Wear */
AKEYCODE_STEM_2 = 266,
/** Generic stem key 3 for Wear */
AKEYCODE_STEM_3 = 267,
/** Directional Pad Up-Left */
AKEYCODE_DPAD_UP_LEFT = 268,
/** Directional Pad Down-Left */
AKEYCODE_DPAD_DOWN_LEFT = 269,
/** Directional Pad Up-Right */
AKEYCODE_DPAD_UP_RIGHT = 270,
/** Directional Pad Down-Right */
AKEYCODE_DPAD_DOWN_RIGHT = 271,
/** Skip forward media key */
AKEYCODE_MEDIA_SKIP_FORWARD = 272,
/** Skip backward media key */
AKEYCODE_MEDIA_SKIP_BACKWARD = 273,
/** Step forward media key.
* Steps media forward one from at a time. */
AKEYCODE_MEDIA_STEP_FORWARD = 274,
/** Step backward media key.
* Steps media backward one from at a time. */
AKEYCODE_MEDIA_STEP_BACKWARD = 275,
/** Put device to sleep unless a wakelock is held. */
AKEYCODE_SOFT_SLEEP = 276,
/** Cut key. */
AKEYCODE_CUT = 277,
/** Copy key. */
AKEYCODE_COPY = 278,
/** Paste key. */
AKEYCODE_PASTE = 279,
/** fingerprint navigation key, up. */
AKEYCODE_SYSTEM_NAVIGATION_UP = 280,
/** fingerprint navigation key, down. */
AKEYCODE_SYSTEM_NAVIGATION_DOWN = 281,
/** fingerprint navigation key, left. */
AKEYCODE_SYSTEM_NAVIGATION_LEFT = 282,
/** fingerprint navigation key, right. */
AKEYCODE_SYSTEM_NAVIGATION_RIGHT = 283,
/** all apps */
AKEYCODE_ALL_APPS = 284
};
#endif // _ANDROID_KEYCODES_H

View file

@ -0,0 +1,229 @@
#include <QApplication>
#include <QClipboard>
#include "controller.h"
#include "controlmsg.h"
#include "inputconvertgame.h"
#include "receiver.h"
#include "videosocket.h"
Controller::Controller(QString gameScript, QObject *parent) : QObject(parent)
{
m_receiver = new Receiver(this);
Q_ASSERT(m_receiver);
updateScript(gameScript);
}
Controller::~Controller() {}
void Controller::setControlSocket(QTcpSocket *controlSocket)
{
if (m_controlSocket || !controlSocket) {
return;
}
m_controlSocket = controlSocket;
m_receiver->setControlSocket(controlSocket);
}
void Controller::postControlMsg(ControlMsg *controlMsg)
{
if (controlMsg) {
QCoreApplication::postEvent(this, controlMsg);
}
}
void Controller::test(QRect rc)
{
ControlMsg *controlMsg = new ControlMsg(ControlMsg::CMT_INJECT_TOUCH);
controlMsg->setInjectTouchMsgData(static_cast<quint64>(POINTER_ID_MOUSE), AMOTION_EVENT_ACTION_DOWN, AMOTION_EVENT_BUTTON_PRIMARY, rc, 1.0f);
postControlMsg(controlMsg);
}
void Controller::updateScript(QString gameScript)
{
if (m_inputConvert) {
delete m_inputConvert;
}
if (!gameScript.isEmpty()) {
InputConvertGame *convertgame = new InputConvertGame(this);
convertgame->loadKeyMap(gameScript);
m_inputConvert = convertgame;
} else {
m_inputConvert = new InputConvertNormal(this);
}
Q_ASSERT(m_inputConvert);
connect(m_inputConvert, &InputConvertBase::grabCursor, this, &Controller::grabCursor);
}
void Controller::onPostBackOrScreenOn()
{
ControlMsg *controlMsg = new ControlMsg(ControlMsg::CMT_BACK_OR_SCREEN_ON);
if (!controlMsg) {
return;
}
postControlMsg(controlMsg);
}
void Controller::onPostGoHome()
{
postKeyCodeClick(AKEYCODE_HOME);
}
void Controller::onPostGoMenu()
{
postKeyCodeClick(AKEYCODE_MENU);
}
void Controller::onPostGoBack()
{
postKeyCodeClick(AKEYCODE_BACK);
}
void Controller::onPostAppSwitch()
{
postKeyCodeClick(AKEYCODE_APP_SWITCH);
}
void Controller::onPostPower()
{
postKeyCodeClick(AKEYCODE_POWER);
}
void Controller::onPostVolumeUp()
{
postKeyCodeClick(AKEYCODE_VOLUME_UP);
}
void Controller::onPostVolumeDown()
{
postKeyCodeClick(AKEYCODE_VOLUME_DOWN);
}
void Controller::onExpandNotificationPanel()
{
ControlMsg *controlMsg = new ControlMsg(ControlMsg::CMT_EXPAND_NOTIFICATION_PANEL);
if (!controlMsg) {
return;
}
postControlMsg(controlMsg);
}
void Controller::onCollapseNotificationPanel()
{
ControlMsg *controlMsg = new ControlMsg(ControlMsg::CMT_COLLAPSE_NOTIFICATION_PANEL);
if (!controlMsg) {
return;
}
postControlMsg(controlMsg);
}
void Controller::onRequestDeviceClipboard()
{
ControlMsg *controlMsg = new ControlMsg(ControlMsg::CMT_GET_CLIPBOARD);
if (!controlMsg) {
return;
}
postControlMsg(controlMsg);
}
void Controller::onSetDeviceClipboard()
{
QClipboard *board = QApplication::clipboard();
QString text = board->text();
ControlMsg *controlMsg = new ControlMsg(ControlMsg::CMT_SET_CLIPBOARD);
if (!controlMsg) {
return;
}
controlMsg->setSetClipboardMsgData(text);
postControlMsg(controlMsg);
}
void Controller::onClipboardPaste()
{
QClipboard *board = QApplication::clipboard();
QString text = board->text();
onPostTextInput(text);
}
void Controller::onPostTextInput(QString &text)
{
ControlMsg *controlMsg = new ControlMsg(ControlMsg::CMT_INJECT_TEXT);
if (!controlMsg) {
return;
}
controlMsg->setInjectTextMsgData(text);
postControlMsg(controlMsg);
}
void Controller::onSetScreenPowerMode(ControlMsg::ScreenPowerMode mode)
{
ControlMsg *controlMsg = new ControlMsg(ControlMsg::CMT_SET_SCREEN_POWER_MODE);
if (!controlMsg) {
return;
}
controlMsg->setSetScreenPowerModeData(mode);
postControlMsg(controlMsg);
}
void Controller::onMouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize)
{
if (m_inputConvert) {
m_inputConvert->mouseEvent(from, frameSize, showSize);
}
}
void Controller::onWheelEvent(const QWheelEvent *from, const QSize &frameSize, const QSize &showSize)
{
if (m_inputConvert) {
m_inputConvert->wheelEvent(from, frameSize, showSize);
}
}
void Controller::onKeyEvent(const QKeyEvent *from, const QSize &frameSize, const QSize &showSize)
{
if (m_inputConvert) {
m_inputConvert->keyEvent(from, frameSize, showSize);
}
}
bool Controller::event(QEvent *event)
{
if (event && static_cast<ControlMsg::Type>(event->type()) == ControlMsg::Control) {
ControlMsg *controlMsg = dynamic_cast<ControlMsg *>(event);
if (controlMsg) {
sendControl(controlMsg->serializeData());
}
return true;
}
return QObject::event(event);
}
bool Controller::sendControl(const QByteArray &buffer)
{
if (buffer.isEmpty()) {
return false;
}
qint32 len = 0;
if (m_controlSocket) {
len = static_cast<qint32>(m_controlSocket->write(buffer.data(), buffer.length()));
}
return len == buffer.length() ? true : false;
}
void Controller::postKeyCodeClick(AndroidKeycode keycode)
{
ControlMsg *controlEventDown = new ControlMsg(ControlMsg::CMT_INJECT_KEYCODE);
if (!controlEventDown) {
return;
}
controlEventDown->setInjectKeycodeMsgData(AKEY_EVENT_ACTION_DOWN, keycode, AMETA_NONE);
postControlMsg(controlEventDown);
ControlMsg *controlEventUp = new ControlMsg(ControlMsg::CMT_INJECT_KEYCODE);
if (!controlEventUp) {
return;
}
controlEventUp->setInjectKeycodeMsgData(AKEY_EVENT_ACTION_UP, keycode, AMETA_NONE);
postControlMsg(controlEventUp);
}

View file

@ -0,0 +1,65 @@
#ifndef CONTROLLER_H
#define CONTROLLER_H
#include <QObject>
#include <QPointer>
#include "inputconvertbase.h"
class QTcpSocket;
class Receiver;
class InputConvertBase;
class Controller : public QObject
{
Q_OBJECT
public:
Controller(QString gameScript = "", QObject *parent = Q_NULLPTR);
virtual ~Controller();
void setControlSocket(QTcpSocket *controlSocket);
void postControlMsg(ControlMsg *controlMsg);
void test(QRect rc);
void updateScript(QString gameScript = "");
public slots:
void onPostGoBack();
void onPostGoHome();
void onPostGoMenu();
void onPostAppSwitch();
void onPostPower();
void onPostVolumeUp();
void onPostVolumeDown();
void onExpandNotificationPanel();
void onCollapseNotificationPanel();
void onSetScreenPowerMode(ControlMsg::ScreenPowerMode mode);
// for input convert
void onMouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize);
void onWheelEvent(const QWheelEvent *from, const QSize &frameSize, const QSize &showSize);
void onKeyEvent(const QKeyEvent *from, const QSize &frameSize, const QSize &showSize);
// turn the screen on if it was off, press BACK otherwise
void onPostBackOrScreenOn();
void onRequestDeviceClipboard();
void onSetDeviceClipboard();
void onClipboardPaste();
void onPostTextInput(QString &text);
signals:
void grabCursor(bool grab);
protected:
bool event(QEvent *event);
private:
bool sendControl(const QByteArray &buffer);
void postKeyCodeClick(AndroidKeycode keycode);
private:
QPointer<QTcpSocket> m_controlSocket;
QPointer<Receiver> m_receiver;
QPointer<InputConvertBase> m_inputConvert;
};
#endif // CONTROLLER_H

View file

@ -0,0 +1,14 @@
HEADERS += \
$$PWD/controller.h
SOURCES += \
$$PWD/controller.cpp
include ($$PWD/receiver/receiver.pri)
include ($$PWD/inputconvert/inputconvert.pri)
INCLUDEPATH += \
$$PWD/receiver \
$$PWD/inputconvert

View file

@ -0,0 +1,144 @@
#include <QDebug>
#include "bufferutil.h"
#include "controlmsg.h"
ControlMsg::ControlMsg(ControlMsgType controlMsgType) : QScrcpyEvent(Control)
{
m_data.type = controlMsgType;
}
ControlMsg::~ControlMsg()
{
if (CMT_SET_CLIPBOARD == m_data.type && Q_NULLPTR != m_data.setClipboard.text) {
delete m_data.setClipboard.text;
m_data.setClipboard.text = Q_NULLPTR;
} else if (CMT_INJECT_TEXT == m_data.type && Q_NULLPTR != m_data.injectText.text) {
delete m_data.injectText.text;
m_data.injectText.text = Q_NULLPTR;
}
}
void ControlMsg::setInjectKeycodeMsgData(AndroidKeyeventAction action, AndroidKeycode keycode, AndroidMetastate metastate)
{
m_data.injectKeycode.action = action;
m_data.injectKeycode.keycode = keycode;
m_data.injectKeycode.metastate = metastate;
}
void ControlMsg::setInjectTextMsgData(QString &text)
{
// write length (2 byte) + string (non nul-terminated)
if (CONTROL_MSG_TEXT_MAX_LENGTH < text.length()) {
// injecting a text takes time, so limit the text length
text = text.left(CONTROL_MSG_TEXT_MAX_LENGTH);
}
QByteArray tmp = text.toUtf8();
m_data.injectText.text = new char[tmp.length() + 1];
memcpy(m_data.injectText.text, tmp.data(), tmp.length());
m_data.injectText.text[tmp.length()] = '\0';
}
void ControlMsg::setInjectTouchMsgData(quint64 id, AndroidMotioneventAction action, AndroidMotioneventButtons buttons, QRect position, float pressure)
{
m_data.injectTouch.id = id;
m_data.injectTouch.action = action;
m_data.injectTouch.buttons = buttons;
m_data.injectTouch.position = position;
m_data.injectTouch.pressure = pressure;
}
void ControlMsg::setInjectScrollMsgData(QRect position, qint32 hScroll, qint32 vScroll)
{
m_data.injectScroll.position = position;
m_data.injectScroll.hScroll = hScroll;
m_data.injectScroll.vScroll = vScroll;
}
void ControlMsg::setSetClipboardMsgData(QString &text)
{
if (text.isEmpty()) {
return;
}
if (CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH < text.length()) {
text = text.left(CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH);
}
QByteArray tmp = text.toUtf8();
m_data.setClipboard.text = new char[tmp.length() + 1];
memcpy(m_data.setClipboard.text, tmp.data(), tmp.length());
m_data.setClipboard.text[tmp.length()] = '\0';
}
void ControlMsg::setSetScreenPowerModeData(ControlMsg::ScreenPowerMode mode)
{
m_data.setScreenPowerMode.mode = mode;
}
void ControlMsg::writePosition(QBuffer &buffer, const QRect &value)
{
BufferUtil::write32(buffer, value.left());
BufferUtil::write32(buffer, value.top());
BufferUtil::write16(buffer, value.width());
BufferUtil::write16(buffer, value.height());
}
quint16 ControlMsg::toFixedPoint16(float f)
{
Q_ASSERT(f >= 0.0f && f <= 1.0f);
quint32 u = f * 0x1p16f; // 2^16
if (u >= 0xffff) {
u = 0xffff;
}
return (quint16)u;
}
QByteArray ControlMsg::serializeData()
{
QByteArray byteArray;
QBuffer buffer(&byteArray);
buffer.open(QBuffer::WriteOnly);
buffer.putChar(m_data.type);
switch (m_data.type) {
case CMT_INJECT_KEYCODE:
buffer.putChar(m_data.injectKeycode.action);
BufferUtil::write32(buffer, m_data.injectKeycode.keycode);
BufferUtil::write32(buffer, m_data.injectKeycode.metastate);
break;
case CMT_INJECT_TEXT:
BufferUtil::write16(buffer, static_cast<quint32>(strlen(m_data.injectText.text)));
buffer.write(m_data.injectText.text, strlen(m_data.injectText.text));
break;
case CMT_INJECT_TOUCH: {
buffer.putChar(m_data.injectTouch.action);
BufferUtil::write64(buffer, m_data.injectTouch.id);
writePosition(buffer, m_data.injectTouch.position);
quint16 pressure = toFixedPoint16(m_data.injectTouch.pressure);
BufferUtil::write16(buffer, pressure);
BufferUtil::write32(buffer, m_data.injectTouch.buttons);
} break;
case CMT_INJECT_SCROLL:
writePosition(buffer, m_data.injectScroll.position);
BufferUtil::write32(buffer, m_data.injectScroll.hScroll);
BufferUtil::write32(buffer, m_data.injectScroll.vScroll);
break;
case CMT_SET_CLIPBOARD:
BufferUtil::write16(buffer, static_cast<quint32>(strlen(m_data.setClipboard.text)));
buffer.write(m_data.setClipboard.text, strlen(m_data.setClipboard.text));
break;
case CMT_SET_SCREEN_POWER_MODE:
buffer.putChar(m_data.setScreenPowerMode.mode);
break;
case CMT_BACK_OR_SCREEN_ON:
case CMT_EXPAND_NOTIFICATION_PANEL:
case CMT_COLLAPSE_NOTIFICATION_PANEL:
case CMT_GET_CLIPBOARD:
break;
default:
qDebug() << "Unknown event type:" << m_data.type;
break;
}
buffer.close();
return byteArray;
}

View file

@ -0,0 +1,107 @@
#ifndef CONTROLMSG_H
#define CONTROLMSG_H
#include <QBuffer>
#include <QRect>
#include <QString>
#include "input.h"
#include "keycodes.h"
#include "qscrcpyevent.h"
#define CONTROL_MSG_TEXT_MAX_LENGTH 300
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093
#define POINTER_ID_MOUSE static_cast<quint64>(-1)
// ControlMsg
class ControlMsg : public QScrcpyEvent
{
public:
enum ControlMsgType
{
CMT_NULL = -1,
CMT_INJECT_KEYCODE = 0,
CMT_INJECT_TEXT,
CMT_INJECT_TOUCH,
CMT_INJECT_SCROLL,
CMT_BACK_OR_SCREEN_ON,
CMT_EXPAND_NOTIFICATION_PANEL,
CMT_COLLAPSE_NOTIFICATION_PANEL,
CMT_GET_CLIPBOARD,
CMT_SET_CLIPBOARD,
CMT_SET_SCREEN_POWER_MODE
};
enum ScreenPowerMode
{
// see <https://android.googlesource.com/platform/frameworks/base.git/+/pie-release-2/core/java/android/view/SurfaceControl.java#305>
SPM_OFF = 0,
SPM_NORMAL = 2,
};
ControlMsg(ControlMsgType controlMsgType);
virtual ~ControlMsg();
void setInjectKeycodeMsgData(AndroidKeyeventAction action, AndroidKeycode keycode, AndroidMetastate metastate);
void setInjectTextMsgData(QString &text);
// id 代表一个触摸点最多支持10个触摸点[0,9]
// action 只能是AMOTION_EVENT_ACTION_DOWNAMOTION_EVENT_ACTION_UPAMOTION_EVENT_ACTION_MOVE
// position action动作对应的位置
void setInjectTouchMsgData(quint64 id, AndroidMotioneventAction action, AndroidMotioneventButtons buttons, QRect position, float pressure);
void setInjectScrollMsgData(QRect position, qint32 hScroll, qint32 vScroll);
void setSetClipboardMsgData(QString &text);
void setSetScreenPowerModeData(ControlMsg::ScreenPowerMode mode);
QByteArray serializeData();
private:
void writePosition(QBuffer &buffer, const QRect &value);
quint16 toFixedPoint16(float f);
private:
struct ControlMsgData
{
ControlMsgType type = CMT_NULL;
union
{
struct
{
AndroidKeyeventAction action;
AndroidKeycode keycode;
AndroidMetastate metastate;
} injectKeycode;
struct
{
char *text = Q_NULLPTR;
} injectText;
struct
{
quint64 id;
AndroidMotioneventAction action;
AndroidMotioneventButtons buttons;
QRect position;
float pressure;
} injectTouch;
struct
{
QRect position;
qint32 hScroll;
qint32 vScroll;
} injectScroll;
struct
{
char *text = Q_NULLPTR;
} setClipboard;
struct
{
ScreenPowerMode mode;
} setScreenPowerMode;
};
ControlMsgData() {}
~ControlMsgData() {}
};
ControlMsgData m_data;
};
#endif // CONTROLMSG_H

View file

@ -0,0 +1,17 @@
HEADERS += \
$$PWD/inputconvertbase.h \
$$PWD/inputconvertgame.h \
$$PWD/inputconvertnormal.h \
$$PWD/controlmsg.h
SOURCES += \
$$PWD/inputconvertbase.cpp \
$$PWD/inputconvertgame.cpp \
$$PWD/inputconvertnormal.cpp \
$$PWD/controlmsg.cpp
include ($$PWD/keymap/keymap.pri)
INCLUDEPATH += \
$$PWD/keymap

View file

@ -0,0 +1,16 @@
#include "inputconvertbase.h"
#include "controller.h"
InputConvertBase::InputConvertBase(Controller *controller) : QObject(controller), m_controller(controller)
{
Q_ASSERT(controller);
}
InputConvertBase::~InputConvertBase() {}
void InputConvertBase::sendControlMsg(ControlMsg *msg)
{
if (msg && m_controller) {
m_controller->postControlMsg(msg);
}
}

View file

@ -0,0 +1,35 @@
#ifndef INPUTCONVERTBASE_H
#define INPUTCONVERTBASE_H
#include <QKeyEvent>
#include <QMouseEvent>
#include <QPointer>
#include <QWheelEvent>
#include "controlmsg.h"
class Controller;
class InputConvertBase : public QObject
{
Q_OBJECT
public:
InputConvertBase(Controller *controller);
virtual ~InputConvertBase();
// the frame size may be different from the real device size, so we need the size
// to which the absolute position apply, to scale it accordingly
virtual void mouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize) = 0;
virtual void wheelEvent(const QWheelEvent *from, const QSize &frameSize, const QSize &showSize) = 0;
virtual void keyEvent(const QKeyEvent *from, const QSize &frameSize, const QSize &showSize) = 0;
signals:
void grabCursor(bool grab);
protected:
void sendControlMsg(ControlMsg *msg);
private:
QPointer<Controller> m_controller;
};
#endif // INPUTCONVERTBASE_H

View file

@ -0,0 +1,484 @@
#include <QDebug>
#include <QCursor>
#include <QGuiApplication>
#include <QTimer>
#include "inputconvertgame.h"
#define CURSOR_POS_CHECK 50
InputConvertGame::InputConvertGame(Controller *controller) : InputConvertNormal(controller) {}
InputConvertGame::~InputConvertGame() {}
void InputConvertGame::mouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize)
{
// 处理开关按键
if (m_keyMap.isSwitchOnKeyboard() == false && m_keyMap.getSwitchKey() == static_cast<int>(from->button())) {
if (from->type() != QEvent::MouseButtonPress) {
return;
}
if (!switchGameMap()) {
m_needSwitchGameAgain = false;
}
return;
}
if (m_gameMap) {
updateSize(frameSize, showSize);
// mouse move
if (m_keyMap.isValidMouseMoveMap()) {
if (processMouseMove(from)) {
return;
}
}
// mouse click
if (processMouseClick(from)) {
return;
}
}
InputConvertNormal::mouseEvent(from, frameSize, showSize);
}
void InputConvertGame::wheelEvent(const QWheelEvent *from, const QSize &frameSize, const QSize &showSize)
{
if (m_gameMap) {
updateSize(frameSize, showSize);
} else {
InputConvertNormal::wheelEvent(from, frameSize, showSize);
}
}
void InputConvertGame::keyEvent(const QKeyEvent *from, const QSize &frameSize, const QSize &showSize)
{
// 处理开关按键
if (m_keyMap.isSwitchOnKeyboard() && m_keyMap.getSwitchKey() == from->key()) {
if (QEvent::KeyPress != from->type()) {
return;
}
if (!switchGameMap()) {
m_needSwitchGameAgain = false;
}
return;
}
const KeyMap::KeyMapNode &node = m_keyMap.getKeyMapNodeKey(from->key());
// 处理特殊按键:可以在按键映射和普通映射间切换的按键
if (m_needSwitchGameAgain && KeyMap::KMT_CLICK == node.type && node.data.click.switchMap) {
updateSize(frameSize, showSize);
// Qt::Key_Tab Qt::Key_M for PUBG mobile
processKeyClick(node.data.click.keyNode.pos, false, node.data.click.switchMap, from);
return;
}
if (m_gameMap) {
updateSize(frameSize, showSize);
if (!from || from->isAutoRepeat()) {
return;
}
// small eyes
if (from->key() == m_keyMap.getMouseMoveMap().data.mouseMove.smallEyes.key) {
m_ctrlMouseMove.smallEyes = (QEvent::KeyPress == from->type());
if (QEvent::KeyPress == from->type()) {
m_processMouseMove = false;
int delay = 30;
QTimer::singleShot(delay, this, [this]() { mouseMoveStopTouch(); });
QTimer::singleShot(delay * 2, this, [this]() {
mouseMoveStartTouch(nullptr);
m_processMouseMove = true;
});
stopMouseMoveTimer();
} else {
mouseMoveStopTouch();
mouseMoveStartTouch(nullptr);
}
return;
}
switch (node.type) {
// 处理方向盘
case KeyMap::KMT_STEER_WHEEL:
processSteerWheel(node, from);
return;
// 处理普通按键
case KeyMap::KMT_CLICK:
processKeyClick(node.data.click.keyNode.pos, false, node.data.click.switchMap, from);
return;
case KeyMap::KMT_CLICK_TWICE:
processKeyClick(node.data.clickTwice.keyNode.pos, true, false, from);
return;
case KeyMap::KMT_DRAG:
processKeyDrag(node.data.drag.keyNode.pos, node.data.drag.keyNode.extendPos, from);
return;
default:
break;
}
} else {
InputConvertNormal::keyEvent(from, frameSize, showSize);
}
}
void InputConvertGame::loadKeyMap(const QString &json)
{
m_keyMap.loadKeyMap(json);
}
void InputConvertGame::updateSize(const QSize &frameSize, const QSize &showSize)
{
if (showSize != m_showSize) {
if (m_gameMap && m_keyMap.isValidMouseMoveMap()) {
// show size change, resize grab cursor
emit grabCursor(true);
}
}
m_frameSize = frameSize;
m_showSize = showSize;
}
void InputConvertGame::sendTouchDownEvent(int id, QPointF pos)
{
sendTouchEvent(id, pos, AMOTION_EVENT_ACTION_DOWN);
}
void InputConvertGame::sendTouchMoveEvent(int id, QPointF pos)
{
sendTouchEvent(id, pos, AMOTION_EVENT_ACTION_MOVE);
}
void InputConvertGame::sendTouchUpEvent(int id, QPointF pos)
{
sendTouchEvent(id, pos, AMOTION_EVENT_ACTION_UP);
}
void InputConvertGame::sendTouchEvent(int id, QPointF pos, AndroidMotioneventAction action)
{
if (0 > id || MULTI_TOUCH_MAX_NUM - 1 < id) {
Q_ASSERT(0);
return;
}
//qDebug() << "id:" << id << " pos:" << pos << " action" << action;
ControlMsg *controlMsg = new ControlMsg(ControlMsg::CMT_INJECT_TOUCH);
if (!controlMsg) {
return;
}
controlMsg->setInjectTouchMsgData(
static_cast<quint64>(id), action, static_cast<AndroidMotioneventButtons>(0), QRect(calcFrameAbsolutePos(pos).toPoint(), m_frameSize), 1.0f);
sendControlMsg(controlMsg);
}
QPointF InputConvertGame::calcFrameAbsolutePos(QPointF relativePos)
{
QPointF absolutePos;
absolutePos.setX(m_frameSize.width() * relativePos.x());
absolutePos.setY(m_frameSize.height() * relativePos.y());
return absolutePos;
}
QPointF InputConvertGame::calcScreenAbsolutePos(QPointF relativePos)
{
QPointF absolutePos;
absolutePos.setX(m_showSize.width() * relativePos.x());
absolutePos.setY(m_showSize.height() * relativePos.y());
return absolutePos;
}
int InputConvertGame::attachTouchID(int key)
{
for (int i = 0; i < MULTI_TOUCH_MAX_NUM; i++) {
if (0 == m_multiTouchID[i]) {
m_multiTouchID[i] = key;
return i;
}
}
return -1;
}
void InputConvertGame::detachTouchID(int key)
{
for (int i = 0; i < MULTI_TOUCH_MAX_NUM; i++) {
if (key == m_multiTouchID[i]) {
m_multiTouchID[i] = 0;
return;
}
}
}
int InputConvertGame::getTouchID(int key)
{
for (int i = 0; i < MULTI_TOUCH_MAX_NUM; i++) {
if (key == m_multiTouchID[i]) {
return i;
}
}
return -1;
}
// -------- steer wheel event --------
void InputConvertGame::processSteerWheel(const KeyMap::KeyMapNode &node, const QKeyEvent *from)
{
int key = from->key();
bool flag = from->type() == QEvent::KeyPress;
// identify keys
if (key == node.data.steerWheel.up.key) {
m_ctrlSteerWheel.pressedUp = flag;
} else if (key == node.data.steerWheel.right.key) {
m_ctrlSteerWheel.pressedRight = flag;
} else if (key == node.data.steerWheel.down.key) {
m_ctrlSteerWheel.pressedDown = flag;
} else { // left
m_ctrlSteerWheel.pressedLeft = flag;
}
// calc offset and pressed number
QPointF offset(0.0, 0.0);
int pressedNum = 0;
if (m_ctrlSteerWheel.pressedUp) {
++pressedNum;
offset.ry() -= node.data.steerWheel.up.extendOffset;
}
if (m_ctrlSteerWheel.pressedRight) {
++pressedNum;
offset.rx() += node.data.steerWheel.right.extendOffset;
}
if (m_ctrlSteerWheel.pressedDown) {
++pressedNum;
offset.ry() += node.data.steerWheel.down.extendOffset;
}
if (m_ctrlSteerWheel.pressedLeft) {
++pressedNum;
offset.rx() -= node.data.steerWheel.left.extendOffset;
}
// action
if (pressedNum == 0) {
// touch up release all
int id = getTouchID(m_ctrlSteerWheel.touchKey);
sendTouchUpEvent(id, node.data.steerWheel.centerPos + m_ctrlSteerWheel.lastOffset);
detachTouchID(m_ctrlSteerWheel.touchKey);
} else {
int id;
// first press, get key and touch down
if (pressedNum == 1 && flag) {
m_ctrlSteerWheel.touchKey = from->key();
id = attachTouchID(m_ctrlSteerWheel.touchKey);
sendTouchDownEvent(id, node.data.steerWheel.centerPos);
} else {
// jsut get touch id and move
id = getTouchID(m_ctrlSteerWheel.touchKey);
}
sendTouchMoveEvent(id, node.data.steerWheel.centerPos + offset);
}
m_ctrlSteerWheel.lastOffset = offset;
return;
}
// -------- key event --------
void InputConvertGame::processKeyClick(const QPointF &clickPos, bool clickTwice, bool switchMap, const QKeyEvent *from)
{
if (switchMap && QEvent::KeyRelease == from->type()) {
m_needSwitchGameAgain = !m_needSwitchGameAgain;
switchGameMap();
}
if (QEvent::KeyPress == from->type()) {
int id = attachTouchID(from->key());
sendTouchDownEvent(id, clickPos);
if (clickTwice) {
sendTouchUpEvent(getTouchID(from->key()), clickPos);
detachTouchID(from->key());
}
} else if (QEvent::KeyRelease == from->type()) {
if (clickTwice) {
int id = attachTouchID(from->key());
sendTouchDownEvent(id, clickPos);
}
sendTouchUpEvent(getTouchID(from->key()), clickPos);
detachTouchID(from->key());
}
}
void InputConvertGame::processKeyDrag(const QPointF &startPos, QPointF endPos, const QKeyEvent *from)
{
if (QEvent::KeyPress == from->type()) {
int id = attachTouchID(from->key());
sendTouchDownEvent(id, startPos);
sendTouchMoveEvent(id, endPos);
}
if (QEvent::KeyRelease == from->type()) {
int id = getTouchID(from->key());
sendTouchUpEvent(id, endPos);
detachTouchID(from->key());
}
}
// -------- mouse event --------
bool InputConvertGame::processMouseClick(const QMouseEvent *from)
{
const KeyMap::KeyMapNode &node = m_keyMap.getKeyMapNodeMouse(from->button());
if (KeyMap::KMT_INVALID == node.type) {
return false;
}
if (QEvent::MouseButtonPress == from->type() || QEvent::MouseButtonDblClick == from->type()) {
int id = attachTouchID(from->button());
sendTouchDownEvent(id, node.data.click.keyNode.pos);
return true;
}
if (QEvent::MouseButtonRelease == from->type()) {
int id = getTouchID(from->button());
sendTouchUpEvent(id, node.data.click.keyNode.pos);
detachTouchID(from->button());
return true;
}
return false;
}
bool InputConvertGame::processMouseMove(const QMouseEvent *from)
{
if (QEvent::MouseMove != from->type()) {
return false;
}
if (checkCursorPos(from)) {
m_ctrlMouseMove.lastPos = QPointF(0.0, 0.0);
return true;
}
if (!m_ctrlMouseMove.lastPos.isNull() && m_processMouseMove) {
QPointF distance = from->localPos() - m_ctrlMouseMove.lastPos;
distance /= m_keyMap.getMouseMoveMap().data.mouseMove.speedRatio;
mouseMoveStartTouch(from);
startMouseMoveTimer();
m_ctrlMouseMove.lastConverPos.setX(m_ctrlMouseMove.lastConverPos.x() + distance.x() / m_showSize.width());
m_ctrlMouseMove.lastConverPos.setY(m_ctrlMouseMove.lastConverPos.y() + distance.y() / m_showSize.height());
if (m_ctrlMouseMove.lastConverPos.x() < 0.1 || m_ctrlMouseMove.lastConverPos.x() > 0.98 || m_ctrlMouseMove.lastConverPos.y() < 0.1
|| m_ctrlMouseMove.lastConverPos.y() > 0.98) {
if (m_ctrlMouseMove.smallEyes) {
m_processMouseMove = false;
int delay = 30;
QTimer::singleShot(delay, this, [this]() { mouseMoveStopTouch(); });
QTimer::singleShot(delay * 2, this, [this]() {
mouseMoveStartTouch(nullptr);
m_processMouseMove = true;
});
} else {
mouseMoveStopTouch();
mouseMoveStartTouch(from);
}
}
sendTouchMoveEvent(getTouchID(Qt::ExtraButton24), m_ctrlMouseMove.lastConverPos);
}
m_ctrlMouseMove.lastPos = from->localPos();
return true;
}
bool InputConvertGame::checkCursorPos(const QMouseEvent *from)
{
bool moveCursor = false;
QPoint pos = from->pos();
if (pos.x() < CURSOR_POS_CHECK) {
pos.setX(m_showSize.width() - CURSOR_POS_CHECK);
moveCursor = true;
} else if (pos.x() > m_showSize.width() - CURSOR_POS_CHECK) {
pos.setX(CURSOR_POS_CHECK);
moveCursor = true;
} else if (pos.y() < CURSOR_POS_CHECK) {
pos.setY(m_showSize.height() - CURSOR_POS_CHECK);
moveCursor = true;
} else if (pos.y() > m_showSize.height() - CURSOR_POS_CHECK) {
pos.setY(CURSOR_POS_CHECK);
moveCursor = true;
}
if (moveCursor) {
moveCursorTo(from, pos);
}
return moveCursor;
}
void InputConvertGame::moveCursorTo(const QMouseEvent *from, const QPoint &localPosPixel)
{
QPoint posOffset = from->pos() - localPosPixel;
QPoint globalPos = from->globalPos();
globalPos -= posOffset;
//qDebug()<<"move cursor to "<<globalPos<<" offset "<<posOffset;
QCursor::setPos(globalPos);
}
void InputConvertGame::mouseMoveStartTouch(const QMouseEvent *from)
{
Q_UNUSED(from)
if (!m_ctrlMouseMove.touching) {
QPointF mouseMoveStartPos
= m_ctrlMouseMove.smallEyes ? m_keyMap.getMouseMoveMap().data.mouseMove.smallEyes.pos : m_keyMap.getMouseMoveMap().data.mouseMove.startPos;
int id = attachTouchID(Qt::ExtraButton24);
sendTouchDownEvent(id, mouseMoveStartPos);
m_ctrlMouseMove.lastConverPos = mouseMoveStartPos;
m_ctrlMouseMove.touching = true;
}
}
void InputConvertGame::mouseMoveStopTouch()
{
if (m_ctrlMouseMove.touching) {
sendTouchUpEvent(getTouchID(Qt::ExtraButton24), m_ctrlMouseMove.lastConverPos);
detachTouchID(Qt::ExtraButton24);
m_ctrlMouseMove.touching = false;
}
}
void InputConvertGame::startMouseMoveTimer()
{
stopMouseMoveTimer();
m_ctrlMouseMove.timer = startTimer(1000);
}
void InputConvertGame::stopMouseMoveTimer()
{
if (0 != m_ctrlMouseMove.timer) {
killTimer(m_ctrlMouseMove.timer);
m_ctrlMouseMove.timer = 0;
}
}
bool InputConvertGame::switchGameMap()
{
m_gameMap = !m_gameMap;
if (!m_keyMap.isValidMouseMoveMap()) {
return m_gameMap;
}
// grab cursor and set cursor only mouse move map
emit grabCursor(m_gameMap);
if (m_gameMap) {
#ifdef QT_NO_DEBUG
QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor));
#else
QGuiApplication::setOverrideCursor(QCursor(Qt::CrossCursor));
#endif
} else {
QGuiApplication::restoreOverrideCursor();
}
return m_gameMap;
}
void InputConvertGame::timerEvent(QTimerEvent *event)
{
if (m_ctrlMouseMove.timer == event->timerId()) {
stopMouseMoveTimer();
mouseMoveStopTouch();
}
}

View file

@ -0,0 +1,95 @@
#ifndef INPUTCONVERTGAME_H
#define INPUTCONVERTGAME_H
#include <QPointF>
#include "inputconvertnormal.h"
#include "keymap.h"
#define MULTI_TOUCH_MAX_NUM 10
class InputConvertGame : public InputConvertNormal
{
Q_OBJECT
public:
InputConvertGame(Controller *controller);
virtual ~InputConvertGame();
virtual void mouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize);
virtual void wheelEvent(const QWheelEvent *from, const QSize &frameSize, const QSize &showSize);
virtual void keyEvent(const QKeyEvent *from, const QSize &frameSize, const QSize &showSize);
void loadKeyMap(const QString &json);
protected:
void updateSize(const QSize &frameSize, const QSize &showSize);
void sendTouchDownEvent(int id, QPointF pos);
void sendTouchMoveEvent(int id, QPointF pos);
void sendTouchUpEvent(int id, QPointF pos);
void sendTouchEvent(int id, QPointF pos, AndroidMotioneventAction action);
QPointF calcFrameAbsolutePos(QPointF relativePos);
QPointF calcScreenAbsolutePos(QPointF relativePos);
// multi touch id
int attachTouchID(int key);
void detachTouchID(int key);
int getTouchID(int key);
// steer wheel
void processSteerWheel(const KeyMap::KeyMapNode &node, const QKeyEvent *from);
// click
void processKeyClick(const QPointF &clickPos, bool clickTwice, bool switchMap, const QKeyEvent *from);
// drag
void processKeyDrag(const QPointF &startPos, QPointF endPos, const QKeyEvent *from);
// mouse
bool processMouseClick(const QMouseEvent *from);
bool processMouseMove(const QMouseEvent *from);
void moveCursorTo(const QMouseEvent *from, const QPoint &localPosPixel);
void mouseMoveStartTouch(const QMouseEvent *from);
void mouseMoveStopTouch();
void startMouseMoveTimer();
void stopMouseMoveTimer();
bool switchGameMap();
bool checkCursorPos(const QMouseEvent *from);
protected:
void timerEvent(QTimerEvent *event);
private:
QSize m_frameSize;
QSize m_showSize;
bool m_gameMap = false;
bool m_needSwitchGameAgain = false;
int m_multiTouchID[MULTI_TOUCH_MAX_NUM] = { 0 };
KeyMap m_keyMap;
bool m_processMouseMove = true;
// steer wheel
struct
{
// the first key pressed
int touchKey = Qt::Key_unknown;
bool pressedUp = false;
bool pressedDown = false;
bool pressedLeft = false;
bool pressedRight = false;
// for last up
QPointF lastOffset;
} m_ctrlSteerWheel;
// mouse move
struct
{
QPointF lastConverPos;
QPointF lastPos = { 0.0, 0.0 };
bool touching = false;
int timer = 0;
bool smallEyes = false;
} m_ctrlMouseMove;
};
#endif // INPUTCONVERTGAME_H

View file

@ -0,0 +1,409 @@
#include <cmath>
#include "inputconvertnormal.h"
InputConvertNormal::InputConvertNormal(Controller *controller) : InputConvertBase(controller) {}
InputConvertNormal::~InputConvertNormal() {}
void InputConvertNormal::mouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize)
{
if (!from) {
return;
}
// action
AndroidMotioneventAction action;
switch (from->type()) {
case QEvent::MouseButtonPress:
action = AMOTION_EVENT_ACTION_DOWN;
break;
case QEvent::MouseButtonRelease:
action = AMOTION_EVENT_ACTION_UP;
break;
case QEvent::MouseMove:
// only support left button drag
if (!(from->buttons() & Qt::LeftButton)) {
return;
}
action = AMOTION_EVENT_ACTION_MOVE;
break;
default:
return;
}
// pos
QPointF pos = from->localPos();
// convert pos
pos.setX(pos.x() * frameSize.width() / showSize.width());
pos.setY(pos.y() * frameSize.height() / showSize.height());
// set data
ControlMsg *controlMsg = new ControlMsg(ControlMsg::CMT_INJECT_TOUCH);
if (!controlMsg) {
return;
}
controlMsg->setInjectTouchMsgData(
static_cast<quint64>(POINTER_ID_MOUSE), action, convertMouseButtons(from->buttons()), QRect(pos.toPoint(), frameSize), 1.0f);
sendControlMsg(controlMsg);
}
void InputConvertNormal::wheelEvent(const QWheelEvent *from, const QSize &frameSize, const QSize &showSize)
{
if (!from || 0 == from->delta()) {
return;
}
// delta
qint32 hScroll = 0;
qint32 vScroll = 0;
switch (from->orientation()) {
case Qt::Horizontal:
hScroll = from->delta() / abs(from->delta()) * 2;
break;
case Qt::Vertical:
vScroll = from->delta() / abs(from->delta()) * 2;
break;
}
// pos
QPointF pos = from->posF();
// convert pos
pos.setX(pos.x() * frameSize.width() / showSize.width());
pos.setY(pos.y() * frameSize.height() / showSize.height());
// set data
ControlMsg *controlMsg = new ControlMsg(ControlMsg::CMT_INJECT_SCROLL);
if (!controlMsg) {
return;
}
controlMsg->setInjectScrollMsgData(QRect(pos.toPoint(), frameSize), hScroll, vScroll);
sendControlMsg(controlMsg);
}
void InputConvertNormal::keyEvent(const QKeyEvent *from, const QSize &frameSize, const QSize &showSize)
{
Q_UNUSED(frameSize)
Q_UNUSED(showSize)
if (!from) {
return;
}
// action
AndroidKeyeventAction action;
switch (from->type()) {
case QEvent::KeyPress:
action = AKEY_EVENT_ACTION_DOWN;
break;
case QEvent::KeyRelease:
action = AKEY_EVENT_ACTION_UP;
break;
default:
return;
}
// key code
AndroidKeycode keyCode = convertKeyCode(from->key(), from->modifiers());
if (AKEYCODE_UNKNOWN == keyCode) {
return;
}
// set data
ControlMsg *controlMsg = new ControlMsg(ControlMsg::CMT_INJECT_KEYCODE);
if (!controlMsg) {
return;
}
controlMsg->setInjectKeycodeMsgData(action, keyCode, convertMetastate(from->modifiers()));
sendControlMsg(controlMsg);
}
AndroidMotioneventButtons InputConvertNormal::convertMouseButtons(Qt::MouseButtons buttonState)
{
quint32 buttons = 0;
if (buttonState & Qt::LeftButton) {
buttons |= AMOTION_EVENT_BUTTON_PRIMARY;
}
if (buttonState & Qt::RightButton) {
buttons |= AMOTION_EVENT_BUTTON_SECONDARY;
}
if (buttonState & Qt::MidButton) {
buttons |= AMOTION_EVENT_BUTTON_TERTIARY;
}
if (buttonState & Qt::XButton1) {
buttons |= AMOTION_EVENT_BUTTON_BACK;
}
if (buttonState & Qt::XButton2) {
buttons |= AMOTION_EVENT_BUTTON_FORWARD;
}
return static_cast<AndroidMotioneventButtons>(buttons);
}
AndroidKeycode InputConvertNormal::convertKeyCode(int key, Qt::KeyboardModifiers modifiers)
{
AndroidKeycode keyCode = AKEYCODE_UNKNOWN;
// functional keys
switch (key) {
case Qt::Key_Return:
keyCode = AKEYCODE_ENTER;
break;
case Qt::Key_Enter:
keyCode = AKEYCODE_NUMPAD_ENTER;
break;
case Qt::Key_Escape:
keyCode = AKEYCODE_ESCAPE;
break;
case Qt::Key_Backspace:
keyCode = AKEYCODE_DEL;
break;
case Qt::Key_Delete:
keyCode = AKEYCODE_FORWARD_DEL;
break;
case Qt::Key_Tab:
keyCode = AKEYCODE_TAB;
break;
case Qt::Key_Home:
keyCode = AKEYCODE_MOVE_HOME;
break;
case Qt::Key_End:
keyCode = AKEYCODE_MOVE_END;
break;
case Qt::Key_PageUp:
keyCode = AKEYCODE_PAGE_UP;
break;
case Qt::Key_PageDown:
keyCode = AKEYCODE_PAGE_DOWN;
break;
case Qt::Key_Left:
keyCode = AKEYCODE_DPAD_LEFT;
break;
case Qt::Key_Right:
keyCode = AKEYCODE_DPAD_RIGHT;
break;
case Qt::Key_Up:
keyCode = AKEYCODE_DPAD_UP;
break;
case Qt::Key_Down:
keyCode = AKEYCODE_DPAD_DOWN;
break;
}
if (AKEYCODE_UNKNOWN != keyCode) {
return keyCode;
}
// if ALT and META are pressed, dont handle letters and space
if (modifiers & (Qt::AltModifier | Qt::MetaModifier)) {
return keyCode;
}
// character keys
switch (key) {
case Qt::Key_A:
keyCode = AKEYCODE_A;
break;
case Qt::Key_B:
keyCode = AKEYCODE_B;
break;
case Qt::Key_C:
keyCode = AKEYCODE_C;
break;
case Qt::Key_D:
keyCode = AKEYCODE_D;
break;
case Qt::Key_E:
keyCode = AKEYCODE_E;
break;
case Qt::Key_F:
keyCode = AKEYCODE_F;
break;
case Qt::Key_G:
keyCode = AKEYCODE_G;
break;
case Qt::Key_H:
keyCode = AKEYCODE_H;
break;
case Qt::Key_I:
keyCode = AKEYCODE_I;
break;
case Qt::Key_J:
keyCode = AKEYCODE_J;
break;
case Qt::Key_K:
keyCode = AKEYCODE_K;
break;
case Qt::Key_L:
keyCode = AKEYCODE_L;
break;
case Qt::Key_M:
keyCode = AKEYCODE_M;
break;
case Qt::Key_N:
keyCode = AKEYCODE_N;
break;
case Qt::Key_O:
keyCode = AKEYCODE_O;
break;
case Qt::Key_P:
keyCode = AKEYCODE_P;
break;
case Qt::Key_Q:
keyCode = AKEYCODE_Q;
break;
case Qt::Key_R:
keyCode = AKEYCODE_R;
break;
case Qt::Key_S:
keyCode = AKEYCODE_S;
break;
case Qt::Key_T:
keyCode = AKEYCODE_T;
break;
case Qt::Key_U:
keyCode = AKEYCODE_U;
break;
case Qt::Key_V:
keyCode = AKEYCODE_V;
break;
case Qt::Key_W:
keyCode = AKEYCODE_W;
break;
case Qt::Key_X:
keyCode = AKEYCODE_X;
break;
case Qt::Key_Y:
keyCode = AKEYCODE_Y;
break;
case Qt::Key_Z:
keyCode = AKEYCODE_Z;
break;
case Qt::Key_0:
keyCode = AKEYCODE_0;
break;
case Qt::Key_1:
case Qt::Key_Exclam:// !
keyCode = AKEYCODE_1;
break;
case Qt::Key_2:
keyCode = AKEYCODE_2;
break;
case Qt::Key_3:
keyCode = AKEYCODE_3;
break;
case Qt::Key_4:
case Qt::Key_Dollar://$
keyCode = AKEYCODE_4;
break;
case Qt::Key_5:
case Qt::Key_Percent:// %
keyCode = AKEYCODE_5;
break;
case Qt::Key_6:
case Qt::Key_AsciiCircum: //^
keyCode = AKEYCODE_6;
break;
case Qt::Key_7:
case Qt::Key_Ampersand: //&
keyCode = AKEYCODE_7;
break;
case Qt::Key_8:
keyCode = AKEYCODE_8;
break;
case Qt::Key_9:
keyCode = AKEYCODE_9;
break;
case Qt::Key_Space:
keyCode = AKEYCODE_SPACE;
break;
case Qt::Key_Comma://,
case Qt::Key_Less://<
keyCode = AKEYCODE_COMMA;
break;
case Qt::Key_Period://.
case Qt::Key_Greater://>
keyCode = AKEYCODE_PERIOD;
break;
case Qt::Key_Minus://-
case Qt::Key_Underscore: //_
keyCode = AKEYCODE_MINUS;
break;
case Qt::Key_Equal://=
keyCode = AKEYCODE_EQUALS;
break;
case Qt::Key_BracketLeft://[
case Qt::Key_BraceLeft: //{
keyCode = AKEYCODE_LEFT_BRACKET;
break;
case Qt::Key_BracketRight://]
case Qt::Key_BraceRight: //}
keyCode = AKEYCODE_RIGHT_BRACKET;
break;
case Qt::Key_Backslash:// \ ????
case Qt::Key_Bar: //|
keyCode = AKEYCODE_BACKSLASH;
break;
case Qt::Key_Semicolon://;
case Qt::Key_Colon: //:
keyCode = AKEYCODE_SEMICOLON;
break;
case Qt::Key_Apostrophe://'
case Qt::Key_QuoteDbl: //"
keyCode = AKEYCODE_APOSTROPHE;
break;
case Qt::Key_Slash:// /
case Qt::Key_Question://?
keyCode = AKEYCODE_SLASH;
break;
case Qt::Key_At://@
keyCode = AKEYCODE_AT;
break;
case Qt::Key_Plus://+
keyCode = AKEYCODE_PLUS;
break;
case Qt::Key_QuoteLeft://`
case Qt::Key_AsciiTilde://~
keyCode = AKEYCODE_GRAVE;
break;
case Qt::Key_NumberSign: //#
keyCode = AKEYCODE_POUND;
break;
case Qt::Key_ParenLeft: //(
keyCode = AKEYCODE_NUMPAD_LEFT_PAREN;
break;
case Qt::Key_ParenRight: //)
keyCode = AKEYCODE_NUMPAD_RIGHT_PAREN;
break;
case Qt::Key_Asterisk: //*
keyCode = AKEYCODE_STAR;
break;
}
return keyCode;
}
AndroidMetastate InputConvertNormal::convertMetastate(Qt::KeyboardModifiers modifiers)
{
int metastate = AMETA_NONE;
if (modifiers & Qt::ShiftModifier) {
metastate |= AMETA_SHIFT_ON;
}
if (modifiers & Qt::ControlModifier) {
metastate |= AMETA_CTRL_ON;
}
if (modifiers & Qt::AltModifier) {
metastate |= AMETA_ALT_ON;
}
if (modifiers & Qt::MetaModifier) {
metastate |= AMETA_META_ON;
}
/*
if (mod & KMOD_NUM) {
metastate |= AMETA_NUM_LOCK_ON;
}
if (mod & KMOD_CAPS) {
metastate |= AMETA_CAPS_LOCK_ON;
}
if (mod & KMOD_MODE) { // Alt Gr
// no mapping?
}
*/
return static_cast<AndroidMetastate>(metastate);
}

View file

@ -0,0 +1,23 @@
#ifndef INPUTCONVERT_H
#define INPUTCONVERT_H
#include "inputconvertbase.h"
class InputConvertNormal : public InputConvertBase
{
Q_OBJECT
public:
InputConvertNormal(Controller *controller);
virtual ~InputConvertNormal();
virtual void mouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize);
virtual void wheelEvent(const QWheelEvent *from, const QSize &frameSize, const QSize &showSize);
virtual void keyEvent(const QKeyEvent *from, const QSize &frameSize, const QSize &showSize);
private:
AndroidMotioneventButtons convertMouseButtons(Qt::MouseButtons buttonState);
AndroidKeycode convertKeyCode(int key, Qt::KeyboardModifiers modifiers);
AndroidMetastate convertMetastate(Qt::KeyboardModifiers modifiers);
};
#endif // INPUTCONVERT_H

View file

@ -0,0 +1,428 @@
#include <QCoreApplication>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMetaEnum>
#include "keymap.h"
QString KeyMap::s_keyMapPath = "";
KeyMap::KeyMap(QObject *parent) : QObject(parent) {}
KeyMap::~KeyMap() {}
const QString &KeyMap::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;
}
void KeyMap::loadKeyMap(const QString &json)
{
QString errorString;
QJsonParseError jsonError;
QJsonDocument jsonDoc;
QJsonObject rootObj;
QPair<ActionType, int> switchKey;
jsonDoc = QJsonDocument::fromJson(json.toUtf8(), &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
errorString = QString("json error: %1").arg(jsonError.errorString());
goto parseError;
}
// switchKey
rootObj = jsonDoc.object();
if (!checkItemString(rootObj, "switchKey")) {
errorString = QString("json error: no find switchKey");
goto parseError;
}
switchKey = getItemKey(rootObj, "switchKey");
if (switchKey.first == AT_INVALID) {
errorString = QString("json error: switchKey invalid");
goto parseError;
}
m_switchKey.type = switchKey.first;
m_switchKey.key = switchKey.second;
// mouseMoveMap
if (checkItemObject(rootObj, "mouseMoveMap")) {
QJsonObject mouseMoveMap = getItemObject(rootObj, "mouseMoveMap");
KeyMapNode keyMapNode;
keyMapNode.type = KMT_MOUSE_MOVE;
if (!checkItemDouble(mouseMoveMap, "speedRatio")) {
errorString = QString("json error: mouseMoveMap on find speedRatio");
goto parseError;
}
keyMapNode.data.mouseMove.speedRatio = static_cast<int>(getItemDouble(mouseMoveMap, "speedRatio"));
if (!checkItemObject(mouseMoveMap, "startPos")) {
errorString = QString("json error: mouseMoveMap on find startPos");
goto parseError;
}
QJsonObject startPos = mouseMoveMap.value("startPos").toObject();
if (checkItemDouble(startPos, "x")) {
keyMapNode.data.mouseMove.startPos.setX(getItemDouble(startPos, "x"));
}
if (checkItemDouble(startPos, "y")) {
keyMapNode.data.mouseMove.startPos.setY(getItemDouble(startPos, "y"));
}
// small eyes
if (checkItemObject(mouseMoveMap, "smallEyes")) {
QJsonObject smallEyes = mouseMoveMap.value("smallEyes").toObject();
if (!smallEyes.contains("type") || !smallEyes.value("type").isString()) {
errorString = QString("json error: smallEyes no find node type");
goto parseError;
}
// type just support KMT_CLICK
KeyMap::KeyMapType type = getItemKeyMapType(smallEyes, "type");
if (KeyMap::KMT_CLICK != type) {
errorString = QString("json error: smallEyes just support KMT_CLICK");
goto parseError;
}
// safe check
if (!checkForClick(smallEyes)) {
errorString = QString("json error: smallEyes node format error");
goto parseError;
}
QPair<ActionType, int> key = getItemKey(smallEyes, "key");
if (key.first == AT_INVALID) {
errorString = QString("json error: keyMapNodes node invalid key: %1").arg(smallEyes.value("key").toString());
goto parseError;
}
keyMapNode.data.mouseMove.smallEyes.type = key.first;
keyMapNode.data.mouseMove.smallEyes.key = key.second;
keyMapNode.data.mouseMove.smallEyes.pos = getItemPos(smallEyes, "pos");
}
m_idxMouseMove = m_keyMapNodes.size();
m_keyMapNodes.push_back(keyMapNode);
}
// keyMapNodes
if (rootObj.contains("keyMapNodes") && rootObj.value("keyMapNodes").isArray()) {
QJsonArray keyMapNodes = rootObj.value("keyMapNodes").toArray();
QJsonObject node;
int size = keyMapNodes.size();
for (int i = 0; i < size; i++) {
if (!keyMapNodes.at(i).isObject()) {
errorString = QString("json error: keyMapNodes node must be json object");
goto parseError;
}
node = keyMapNodes.at(i).toObject();
if (!node.contains("type") || !node.value("type").isString()) {
errorString = QString("json error: keyMapNodes no find node type");
goto parseError;
}
KeyMap::KeyMapType type = getItemKeyMapType(node, "type");
switch (type) {
case KeyMap::KMT_CLICK: {
// safe check
if (!checkForClick(node)) {
qWarning() << "json error: keyMapNodes node format error";
break;
}
QPair<ActionType, int> key = getItemKey(node, "key");
if (key.first == AT_INVALID) {
qWarning() << "json error: keyMapNodes node invalid key: " << node.value("key").toString();
break;
}
KeyMapNode keyMapNode;
keyMapNode.type = type;
keyMapNode.data.click.keyNode.type = key.first;
keyMapNode.data.click.keyNode.key = key.second;
keyMapNode.data.click.keyNode.pos = getItemPos(node, "pos");
keyMapNode.data.click.switchMap = getItemBool(node, "switchMap");
m_keyMapNodes.push_back(keyMapNode);
} break;
case KeyMap::KMT_CLICK_TWICE: {
// safe check
if (!checkForClickTwice(node)) {
qWarning() << "json error: keyMapNodes node format error";
break;
}
QPair<ActionType, int> key = getItemKey(node, "key");
if (key.first == AT_INVALID) {
qWarning() << "json error: keyMapNodes node invalid key: " << node.value("key").toString();
break;
}
KeyMapNode keyMapNode;
keyMapNode.type = type;
keyMapNode.data.click.keyNode.type = key.first;
keyMapNode.data.click.keyNode.key = key.second;
keyMapNode.data.click.keyNode.pos = getItemPos(node, "pos");
keyMapNode.data.click.switchMap = getItemBool(node, "switchMap");
m_keyMapNodes.push_back(keyMapNode);
} break;
case KeyMap::KMT_STEER_WHEEL: {
// safe check
if (!checkForSteerWhell(node)) {
qWarning() << "json error: keyMapNodes node format error";
break;
}
QPair<ActionType, int> leftKey = getItemKey(node, "leftKey");
QPair<ActionType, int> rightKey = getItemKey(node, "rightKey");
QPair<ActionType, int> upKey = getItemKey(node, "upKey");
QPair<ActionType, int> downKey = getItemKey(node, "downKey");
if (leftKey.first == AT_INVALID || rightKey.first == AT_INVALID || upKey.first == AT_INVALID || downKey.first == AT_INVALID) {
if (leftKey.first == AT_INVALID) {
qWarning() << "json error: keyMapNodes node invalid key: " << node.value("leftKey").toString();
}
if (rightKey.first == AT_INVALID) {
qWarning() << "json error: keyMapNodes node invalid key: " << node.value("rightKey").toString();
}
if (upKey.first == AT_INVALID) {
qWarning() << "json error: keyMapNodes node invalid key: " << node.value("upKey").toString();
}
if (downKey.first == AT_INVALID) {
qWarning() << "json error: keyMapNodes node invalid key: " << node.value("downKey").toString();
}
break;
}
KeyMapNode keyMapNode;
keyMapNode.type = type;
keyMapNode.data.steerWheel.left = { leftKey.first, leftKey.second, QPointF(0, 0), QPointF(0, 0), getItemDouble(node, "leftOffset") };
keyMapNode.data.steerWheel.right = { rightKey.first, rightKey.second, QPointF(0, 0), QPointF(0, 0), getItemDouble(node, "rightOffset") };
keyMapNode.data.steerWheel.up = { upKey.first, upKey.second, QPointF(0, 0), QPointF(0, 0), getItemDouble(node, "upOffset") };
keyMapNode.data.steerWheel.down = { downKey.first, downKey.second, QPointF(0, 0), QPointF(0, 0), getItemDouble(node, "downOffset") };
keyMapNode.data.steerWheel.centerPos = getItemPos(node, "centerPos");
m_idxSteerWheel = m_keyMapNodes.size();
m_keyMapNodes.push_back(keyMapNode);
} break;
case KeyMap::KMT_DRAG: {
// safe check
if (!checkForDrag(node)) {
qWarning() << "json error: keyMapNodes node format error";
break;
}
QPair<ActionType, int> key = getItemKey(node, "key");
if (key.first == AT_INVALID) {
qWarning() << "json error: keyMapNodes node invalid key: " << node.value("key").toString();
break;
}
KeyMapNode keyMapNode;
keyMapNode.type = type;
keyMapNode.data.drag.keyNode.type = key.first;
keyMapNode.data.drag.keyNode.key = key.second;
keyMapNode.data.drag.keyNode.pos = getItemPos(node, "startPos");
keyMapNode.data.drag.keyNode.extendPos = getItemPos(node, "endPos");
m_keyMapNodes.push_back(keyMapNode);
break;
}
default:
qWarning() << "json error: keyMapNodes invalid node type:" << node.value("type").toString();
break;
}
}
}
// this must be called after m_keyMapNodes is stable
makeReverseMap();
qInfo() << "Script updated.";
parseError:
if (!errorString.isEmpty()) {
qWarning() << errorString;
}
return;
}
const KeyMap::KeyMapNode &KeyMap::getKeyMapNode(int key)
{
auto p = m_rmapKey.value(key, &m_invalidNode);
if (p == &m_invalidNode) {
return *m_rmapMouse.value(key, &m_invalidNode);
}
return *p;
}
const KeyMap::KeyMapNode &KeyMap::getKeyMapNodeKey(int key)
{
return *m_rmapKey.value(key, &m_invalidNode);
}
const KeyMap::KeyMapNode &KeyMap::getKeyMapNodeMouse(int key)
{
return *m_rmapMouse.value(key, &m_invalidNode);
}
bool KeyMap::isSwitchOnKeyboard()
{
return m_switchKey.type == AT_KEY;
}
int KeyMap::getSwitchKey()
{
return m_switchKey.key;
}
const KeyMap::KeyMapNode &KeyMap::getMouseMoveMap()
{
return m_keyMapNodes[m_idxMouseMove];
}
bool KeyMap::isValidMouseMoveMap()
{
return m_idxMouseMove != -1;
}
bool KeyMap::isValidSteerWheelMap()
{
return m_idxSteerWheel != -1;
}
void KeyMap::makeReverseMap()
{
m_rmapKey.clear();
m_rmapMouse.clear();
for (int i = 0; i < m_keyMapNodes.size(); ++i) {
auto &node = m_keyMapNodes[i];
switch (node.type) {
case KMT_CLICK: {
QMultiHash<int, KeyMapNode *> &m = node.data.click.keyNode.type == AT_KEY ? m_rmapKey : m_rmapMouse;
m.insert(node.data.click.keyNode.key, &node);
} break;
case KMT_CLICK_TWICE: {
QMultiHash<int, KeyMapNode *> &m = node.data.clickTwice.keyNode.type == AT_KEY ? m_rmapKey : m_rmapMouse;
m.insert(node.data.clickTwice.keyNode.key, &node);
} break;
case KMT_STEER_WHEEL: {
QMultiHash<int, KeyMapNode *> &ml = node.data.steerWheel.left.type == AT_KEY ? m_rmapKey : m_rmapMouse;
ml.insert(node.data.steerWheel.left.key, &node);
QMultiHash<int, KeyMapNode *> &mr = node.data.steerWheel.right.type == AT_KEY ? m_rmapKey : m_rmapMouse;
mr.insert(node.data.steerWheel.right.key, &node);
QMultiHash<int, KeyMapNode *> &mu = node.data.steerWheel.up.type == AT_KEY ? m_rmapKey : m_rmapMouse;
mu.insert(node.data.steerWheel.up.key, &node);
QMultiHash<int, KeyMapNode *> &md = node.data.steerWheel.down.type == AT_KEY ? m_rmapKey : m_rmapMouse;
md.insert(node.data.steerWheel.down.key, &node);
} break;
case KMT_DRAG: {
QMultiHash<int, KeyMapNode *> &m = node.data.drag.keyNode.type == AT_KEY ? m_rmapKey : m_rmapMouse;
m.insert(node.data.drag.keyNode.key, &node);
} break;
default:
break;
}
}
}
QString KeyMap::getItemString(const QJsonObject &node, const QString &name)
{
return node.value(name).toString();
}
double KeyMap::getItemDouble(const QJsonObject &node, const QString &name)
{
return node.value(name).toDouble();
}
bool KeyMap::getItemBool(const QJsonObject &node, const QString &name)
{
return node.value(name).toBool(false);
}
QJsonObject KeyMap::getItemObject(const QJsonObject &node, const QString &name)
{
return node.value(name).toObject();
}
QPointF KeyMap::getItemPos(const QJsonObject &node, const QString &name)
{
QJsonObject pos = node.value(name).toObject();
return QPointF(pos.value("x").toDouble(), pos.value("y").toDouble());
}
QPair<KeyMap::ActionType, int> KeyMap::getItemKey(const QJsonObject &node, const QString &name)
{
QString value = getItemString(node, name);
int key = m_metaEnumKey.keyToValue(value.toStdString().c_str());
int btn = m_metaEnumMouseButtons.keyToValue(value.toStdString().c_str());
if (key == -1 && btn == -1) {
return { AT_INVALID, -1 };
} else if (key != -1) {
return { AT_KEY, key };
} else {
return { AT_MOUSE, btn };
}
}
KeyMap::KeyMapType KeyMap::getItemKeyMapType(const QJsonObject &node, const QString &name)
{
QString value = getItemString(node, name);
return static_cast<KeyMap::KeyMapType>(m_metaEnumKeyMapType.keyToValue(value.toStdString().c_str()));
}
bool KeyMap::checkItemString(const QJsonObject &node, const QString &name)
{
return node.contains(name) && node.value(name).isString();
}
bool KeyMap::checkItemDouble(const QJsonObject &node, const QString &name)
{
return node.contains(name) && node.value(name).isDouble();
}
bool KeyMap::checkItemBool(const QJsonObject &node, const QString &name)
{
return node.contains(name) && node.value(name).isBool();
}
bool KeyMap::checkItemObject(const QJsonObject &node, const QString &name)
{
return node.contains(name) && node.value(name).isObject();
}
bool KeyMap::checkItemPos(const QJsonObject &node, const QString &name)
{
if (node.contains(name) && node.value(name).isObject()) {
QJsonObject pos = node.value(name).toObject();
return pos.contains("x") && pos.value("x").isDouble() && pos.contains("y") && pos.value("y").isDouble();
}
return false;
}
bool KeyMap::checkForClick(const QJsonObject &node)
{
return checkForClickTwice(node) && checkItemBool(node, "switchMap");
}
bool KeyMap::checkForClickTwice(const QJsonObject &node)
{
return checkItemString(node, "key") && checkItemPos(node, "pos");
}
bool KeyMap::checkForSteerWhell(const QJsonObject &node)
{
return checkItemString(node, "leftKey") && checkItemString(node, "rightKey") && checkItemString(node, "upKey") && checkItemString(node, "downKey")
&& checkItemDouble(node, "leftOffset") && checkItemDouble(node, "rightOffset") && checkItemDouble(node, "upOffset")
&& checkItemDouble(node, "downOffset") && checkItemPos(node, "centerPos");
}
bool KeyMap::checkForDrag(const QJsonObject &node)
{
return checkItemString(node, "key") && checkItemPos(node, "startPos") && checkItemPos(node, "endPos");
}

View file

@ -0,0 +1,156 @@
#ifndef KEYMAP_H
#define KEYMAP_H
#include <QJsonObject>
#include <QMetaEnum>
#include <QMultiHash>
#include <QObject>
#include <QPair>
#include <QPointF>
#include <QRectF>
#include <QVector>
class KeyMap : public QObject
{
Q_OBJECT
public:
enum KeyMapType
{
KMT_INVALID = -1,
KMT_CLICK = 0,
KMT_CLICK_TWICE,
KMT_STEER_WHEEL,
KMT_DRAG,
KMT_MOUSE_MOVE
};
Q_ENUM(KeyMapType)
enum ActionType
{
AT_INVALID = -1,
AT_KEY = 0,
AT_MOUSE = 1,
};
Q_ENUM(ActionType)
struct KeyNode
{
ActionType type = AT_INVALID;
int key = Qt::Key_unknown;
QPointF pos = QPointF(0, 0); // normal key
QPointF extendPos = QPointF(0, 0); // for drag
double extendOffset = 0.0; // for steerWheel
KeyNode(
ActionType type = AT_INVALID,
int key = Qt::Key_unknown,
QPointF pos = QPointF(0, 0),
QPointF extendPos = QPointF(0, 0),
double extendOffset = 0.0)
: type(type), key(key), pos(pos), extendPos(extendPos), extendOffset(extendOffset)
{
}
};
struct KeyMapNode
{
KeyMapType type = KMT_INVALID;
union DATA
{
struct
{
KeyNode keyNode;
bool switchMap = false;
} click;
struct
{
KeyNode keyNode;
} clickTwice;
struct
{
QPointF centerPos = { 0.0, 0.0 };
KeyNode left, right, up, down;
} steerWheel;
struct
{
KeyNode keyNode;
} drag;
struct
{
QPointF startPos = { 0.0, 0.0 };
int speedRatio = 1;
KeyNode smallEyes;
} mouseMove;
DATA() {}
~DATA() {}
} data;
KeyMapNode() {}
~KeyMapNode() {}
};
KeyMap(QObject *parent = Q_NULLPTR);
virtual ~KeyMap();
void loadKeyMap(const QString &json);
const KeyMap::KeyMapNode &getKeyMapNode(int key);
const KeyMap::KeyMapNode &getKeyMapNodeKey(int key);
const KeyMap::KeyMapNode &getKeyMapNodeMouse(int key);
bool isSwitchOnKeyboard();
int getSwitchKey();
bool isValidMouseMoveMap();
bool isValidSteerWheelMap();
const KeyMap::KeyMapNode &getMouseMoveMap();
static const QString &getKeyMapPath();
private:
// set up the reverse map from key/event event to keyMapNode
void makeReverseMap();
// safe check for base
bool checkItemString(const QJsonObject &node, const QString &name);
bool checkItemDouble(const QJsonObject &node, const QString &name);
bool checkItemBool(const QJsonObject &node, const QString &name);
bool checkItemObject(const QJsonObject &node, const QString &name);
bool checkItemPos(const QJsonObject &node, const QString &name);
// safe check for KeyMapNode
bool checkForClick(const QJsonObject &node);
bool checkForClickTwice(const QJsonObject &node);
bool checkForSteerWhell(const QJsonObject &node);
bool checkForDrag(const QJsonObject &node);
// get keymap from json object
QString getItemString(const QJsonObject &node, const QString &name);
double getItemDouble(const QJsonObject &node, const QString &name);
bool getItemBool(const QJsonObject &node, const QString &name);
QJsonObject getItemObject(const QJsonObject &node, const QString &name);
QPointF getItemPos(const QJsonObject &node, const QString &name);
QPair<ActionType, int> getItemKey(const QJsonObject &node, const QString &name);
KeyMapType getItemKeyMapType(const QJsonObject &node, const QString &name);
private:
static QString s_keyMapPath;
QVector<KeyMapNode> m_keyMapNodes;
KeyNode m_switchKey = { AT_KEY, Qt::Key_QuoteLeft };
// just for return
KeyMapNode m_invalidNode;
// steer wheel index
int m_idxSteerWheel = -1;
// mouse move index
int m_idxMouseMove = -1;
// mapping of key/mouse event name to index
QMetaEnum m_metaEnumKey = QMetaEnum::fromType<Qt::Key>();
QMetaEnum m_metaEnumMouseButtons = QMetaEnum::fromType<Qt::MouseButtons>();
QMetaEnum m_metaEnumKeyMapType = QMetaEnum::fromType<KeyMap::KeyMapType>();
// reverse map of key/mouse event
QMultiHash<int, KeyMapNode *> m_rmapKey;
QMultiHash<int, KeyMapNode *> m_rmapMouse;
};
#endif // KEYMAP_H

View file

@ -0,0 +1,5 @@
HEADERS += \
$$PWD/keymap.h
SOURCES += \
$$PWD/keymap.cpp

View file

@ -0,0 +1,66 @@
#include <QDebug>
#include "bufferutil.h"
#include "devicemsg.h"
DeviceMsg::DeviceMsg(QObject *parent) : QObject(parent) {}
DeviceMsg::~DeviceMsg()
{
if (DMT_GET_CLIPBOARD == m_data.type && Q_NULLPTR != m_data.clipboardMsg.text) {
delete m_data.clipboardMsg.text;
m_data.clipboardMsg.text = Q_NULLPTR;
}
}
DeviceMsg::DeviceMsgType DeviceMsg::type()
{
return m_data.type;
}
void DeviceMsg::getClipboardMsgData(QString &text)
{
text = QString::fromUtf8(m_data.clipboardMsg.text);
}
qint32 DeviceMsg::deserialize(QByteArray &byteArray)
{
QBuffer buf(&byteArray);
buf.open(QBuffer::ReadOnly);
qint64 len = buf.size();
char c = 0;
qint32 ret = 0;
if (len < 3) {
// at least type + empty string length
return 0; // not available
}
buf.getChar(&c);
m_data.type = (DeviceMsgType)c;
switch (m_data.type) {
case DMT_GET_CLIPBOARD: {
m_data.clipboardMsg.text = Q_NULLPTR;
quint16 clipboardLen = BufferUtil::read16(buf);
if (clipboardLen > len - 3) {
ret = 0; // not available
break;
}
QByteArray text = buf.readAll();
m_data.clipboardMsg.text = new char[text.length() + 1];
memcpy(m_data.clipboardMsg.text, text.data(), text.length());
m_data.clipboardMsg.text[text.length()] = '\0';
ret = 3 + clipboardLen;
break;
}
default:
qWarning("Unsupported device msg type: %d", (int)m_data.type);
ret = -1; // error, we cannot recover
}
buf.close();
return ret;
}

View file

@ -0,0 +1,46 @@
#ifndef DEVICEMSG_H
#define DEVICEMSG_H
#include <QBuffer>
#define DEVICE_MSG_QUEUE_SIZE 64
#define DEVICE_MSG_TEXT_MAX_LENGTH 4093
#define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH)
class DeviceMsg : public QObject
{
Q_OBJECT
public:
enum DeviceMsgType
{
DMT_NULL = -1,
// 和服务端对应
DMT_GET_CLIPBOARD = 0,
};
explicit DeviceMsg(QObject *parent = nullptr);
virtual ~DeviceMsg();
DeviceMsg::DeviceMsgType type();
void getClipboardMsgData(QString &text);
qint32 deserialize(QByteArray &byteArray);
private:
struct DeviceMsgData
{
DeviceMsgType type = DMT_NULL;
union
{
struct
{
char *text = Q_NULLPTR;
} clipboardMsg;
};
DeviceMsgData() {}
~DeviceMsgData() {}
};
DeviceMsgData m_data;
};
#endif // DEVICEMSG_H

View file

@ -0,0 +1,53 @@
#include <QApplication>
#include <QClipboard>
#include <QTcpSocket>
#include "devicemsg.h"
#include "receiver.h"
Receiver::Receiver(QObject *parent) : QObject(parent) {}
Receiver::~Receiver() {}
void Receiver::setControlSocket(QTcpSocket *controlSocket)
{
if (m_controlSocket || !controlSocket) {
return;
}
m_controlSocket = controlSocket;
connect(controlSocket, &QTcpSocket::readyRead, this, &Receiver::onReadyRead);
}
void Receiver::onReadyRead()
{
if (!m_controlSocket) {
return;
}
while (m_controlSocket->bytesAvailable()) {
QByteArray byteArray = m_controlSocket->peek(m_controlSocket->bytesAvailable());
DeviceMsg deviceMsg;
qint32 consume = deviceMsg.deserialize(byteArray);
if (0 >= consume) {
break;
}
m_controlSocket->read(consume);
processMsg(&deviceMsg);
}
}
void Receiver::processMsg(DeviceMsg *deviceMsg)
{
switch (deviceMsg->type()) {
case DeviceMsg::DMT_GET_CLIPBOARD: {
qInfo("Device clipboard copied");
QClipboard *board = QApplication::clipboard();
QString text;
deviceMsg->getClipboardMsgData(text);
board->setText(text);
break;
}
default:
break;
}
}

View file

@ -0,0 +1,27 @@
#ifndef RECEIVER_H
#define RECEIVER_H
#include <QPointer>
class QTcpSocket;
class DeviceMsg;
class Receiver : public QObject
{
Q_OBJECT
public:
explicit Receiver(QObject *parent = Q_NULLPTR);
virtual ~Receiver();
void setControlSocket(QTcpSocket *controlSocket);
public slots:
void onReadyRead();
protected:
void processMsg(DeviceMsg *deviceMsg);
private:
QPointer<QTcpSocket> m_controlSocket;
};
#endif // RECEIVER_H

View file

@ -0,0 +1,7 @@
HEADERS += \
$$PWD/devicemsg.h \
$$PWD/receiver.h
SOURCES += \
$$PWD/devicemsg.cpp \
$$PWD/receiver.cpp

View file

@ -0,0 +1,74 @@
#include <QDebug>
#include "avframeconvert.h"
AVFrameConvert::AVFrameConvert() {}
AVFrameConvert::~AVFrameConvert() {}
void AVFrameConvert::setSrcFrameInfo(int srcWidth, int srcHeight, AVPixelFormat srcFormat)
{
m_srcWidth = srcWidth;
m_srcHeight = srcHeight;
m_srcFormat = srcFormat;
qDebug() << "Convert::src frame info " << srcWidth << "x" << srcHeight;
}
void AVFrameConvert::getSrcFrameInfo(int &srcWidth, int &srcHeight, AVPixelFormat &srcFormat)
{
srcWidth = m_srcWidth;
srcHeight = m_srcHeight;
srcFormat = m_srcFormat;
}
void AVFrameConvert::setDstFrameInfo(int dstWidth, int dstHeight, AVPixelFormat dstFormat)
{
m_dstWidth = dstWidth;
m_dstHeight = dstHeight;
m_dstFormat = dstFormat;
}
void AVFrameConvert::getDstFrameInfo(int &dstWidth, int &dstHeight, AVPixelFormat &dstFormat)
{
dstWidth = m_dstWidth;
dstHeight = m_dstHeight;
dstFormat = m_dstFormat;
}
bool AVFrameConvert::init()
{
if (m_convertCtx) {
return true;
}
m_convertCtx = sws_getContext(m_srcWidth, m_srcHeight, m_srcFormat, m_dstWidth, m_dstHeight, m_dstFormat, SWS_BICUBIC, Q_NULLPTR, Q_NULLPTR, Q_NULLPTR);
if (!m_convertCtx) {
return false;
}
return true;
}
bool AVFrameConvert::isInit()
{
return m_convertCtx ? true : false;
}
void AVFrameConvert::deInit()
{
if (m_convertCtx) {
sws_freeContext(m_convertCtx);
m_convertCtx = Q_NULLPTR;
}
}
bool AVFrameConvert::convert(const AVFrame *srcFrame, AVFrame *dstFrame)
{
if (!m_convertCtx || !srcFrame || !dstFrame) {
return false;
}
qint32 ret
= sws_scale(m_convertCtx, static_cast<const uint8_t *const *>(srcFrame->data), srcFrame->linesize, 0, m_srcHeight, dstFrame->data, dstFrame->linesize);
if (0 == ret) {
return false;
}
return true;
}

View file

@ -0,0 +1,40 @@
#ifndef AVFRAMECONVERT_H
#define AVFRAMECONVERT_H
#include <QtGlobal>
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavutil/frame.h"
#include "libswscale/swscale.h"
}
class AVFrameConvert
{
public:
AVFrameConvert();
virtual ~AVFrameConvert();
public:
void setSrcFrameInfo(int srcWidth, int srcHeight, AVPixelFormat srcFormat);
void getSrcFrameInfo(int &srcWidth, int &srcHeight, AVPixelFormat &srcFormat);
void setDstFrameInfo(int dstWidth, int dstHeight, AVPixelFormat dstFormat);
void getDstFrameInfo(int &dstWidth, int &dstHeight, AVPixelFormat &dstFormat);
bool init();
bool isInit();
void deInit();
bool convert(const AVFrame *srcFrame, AVFrame *dstFrame);
private:
int m_srcWidth = 0;
int m_srcHeight = 0;
AVPixelFormat m_srcFormat = AV_PIX_FMT_NONE;
int m_dstWidth = 0;
int m_dstHeight = 0;
AVPixelFormat m_dstFormat = AV_PIX_FMT_NONE;
struct SwsContext *m_convertCtx = Q_NULLPTR;
};
#endif // AVFRAMECONVERT_H

View file

@ -0,0 +1,120 @@
#include <QDebug>
#include "compat.h"
#include "decoder.h"
#include "videobuffer.h"
Decoder::Decoder(VideoBuffer *vb, QObject *parent) : QObject(parent), m_vb(vb) {}
Decoder::~Decoder() {}
bool Decoder::open(const AVCodec *codec)
{
// codec context
m_codecCtx = avcodec_alloc_context3(codec);
if (!m_codecCtx) {
qCritical("Could not allocate decoder context");
return false;
}
if (avcodec_open2(m_codecCtx, codec, NULL) < 0) {
qCritical("Could not open H.264 codec");
return false;
}
m_isCodecCtxOpen = true;
return true;
}
void Decoder::close()
{
if (!m_codecCtx) {
return;
}
if (m_isCodecCtxOpen) {
avcodec_close(m_codecCtx);
}
avcodec_free_context(&m_codecCtx);
}
bool Decoder::push(const AVPacket *packet)
{
if (!m_codecCtx || !m_vb) {
return false;
}
AVFrame *decodingFrame = m_vb->decodingFrame();
#ifdef QTSCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API
int ret = -1;
if ((ret = avcodec_send_packet(m_codecCtx, packet)) < 0) {
char errorbuf[255] = { 0 };
av_strerror(ret, errorbuf, 254);
qCritical("Could not send video packet: %s", errorbuf);
return false;
}
if (decodingFrame) {
ret = avcodec_receive_frame(m_codecCtx, decodingFrame);
}
if (!ret) {
// a frame was received
pushFrame();
//emit getOneFrame(yuvDecoderFrame->data[0], yuvDecoderFrame->data[1], yuvDecoderFrame->data[2],
// yuvDecoderFrame->linesize[0], yuvDecoderFrame->linesize[1], yuvDecoderFrame->linesize[2]);
/*
// m_conver转换yuv为rgb是使用cpu转的占用cpu太高改用opengl渲染yuv
// QImage的copy也非常占用内存此方案不考虑
if (!m_conver.isInit()) {
qDebug() << "decoder frame format" << decodingFrame->format;
m_conver.setSrcFrameInfo(codecCtx->width, codecCtx->height, AV_PIX_FMT_YUV420P);
m_conver.setDstFrameInfo(codecCtx->width, codecCtx->height, AV_PIX_FMT_RGB32);
m_conver.init();
}
if (!outBuffer) {
outBuffer=new quint8[avpicture_get_size(AV_PIX_FMT_RGB32, codecCtx->width, codecCtx->height)];
avpicture_fill((AVPicture *)rgbDecoderFrame, outBuffer, AV_PIX_FMT_RGB32, codecCtx->width, codecCtx->height);
}
m_conver.convert(decodingFrame, rgbDecoderFrame);
//QImage tmpImg((uchar *)outBuffer, codecCtx->width, codecCtx->height, QImage::Format_RGB32);
//QImage image = tmpImg.copy();
//emit getOneImage(image);
*/
} else if (ret != AVERROR(EAGAIN)) {
qCritical("Could not receive video frame: %d", ret);
return false;
}
#else
int gotPicture = 0;
int len = -1;
if (decodingFrame) {
len = avcodec_decode_video2(m_codecCtx, decodingFrame, &gotPicture, packet);
}
if (len < 0) {
qCritical("Could not decode video packet: %d", len);
return false;
}
if (gotPicture) {
pushFrame();
}
#endif
return true;
}
void Decoder::interrupt()
{
if (m_vb) {
m_vb->interrupt();
}
}
void Decoder::pushFrame()
{
if (!m_vb) {
return;
}
bool previousFrameSkipped = true;
m_vb->offerDecodedFrame(previousFrameSkipped);
if (previousFrameSkipped) {
// the previous newFrame will consume this frame
return;
}
emit onNewFrame();
}

View file

@ -0,0 +1,35 @@
#ifndef DECODER_H
#define DECODER_H
#include <QObject>
extern "C"
{
#include "libavcodec/avcodec.h"
}
class VideoBuffer;
class Decoder : public QObject
{
Q_OBJECT
public:
Decoder(VideoBuffer *vb, QObject *parent = Q_NULLPTR);
virtual ~Decoder();
bool open(const AVCodec *codec);
void close();
bool push(const AVPacket *packet);
void interrupt();
signals:
void onNewFrame();
protected:
void pushFrame();
private:
VideoBuffer *m_vb = Q_NULLPTR;
AVCodecContext *m_codecCtx = Q_NULLPTR;
bool m_isCodecCtxOpen = false;
};
#endif // DECODER_H

View file

@ -0,0 +1,11 @@
HEADERS += \
$$PWD/decoder.h \
$$PWD/fpscounter.h \
$$PWD/avframeconvert.h \
$$PWD/videobuffer.h
SOURCES += \
$$PWD/decoder.cpp \
$$PWD/fpscounter.cpp \
$$PWD/avframeconvert.cpp \
$$PWD/videobuffer.cpp

View file

@ -0,0 +1,66 @@
#include <QDebug>
#include <QTimerEvent>
#include "fpscounter.h"
FpsCounter::FpsCounter(QObject *parent) : QObject(parent) {}
FpsCounter::~FpsCounter() {}
void FpsCounter::start()
{
resetCounter();
startCounterTimer();
}
void FpsCounter::stop()
{
stopCounterTimer();
resetCounter();
}
bool FpsCounter::isStarted()
{
return m_counterTimer;
}
void FpsCounter::addRenderedFrame()
{
m_rendered++;
}
void FpsCounter::addSkippedFrame()
{
m_skipped++;
}
void FpsCounter::timerEvent(QTimerEvent *event)
{
if (event && m_counterTimer == event->timerId()) {
m_curRendered = m_rendered;
m_curSkipped = m_skipped;
resetCounter();
emit updateFPS(m_curRendered);
//qInfo("FPS:%d Discard:%d", m_curRendered, m_skipped);
}
}
void FpsCounter::startCounterTimer()
{
stopCounterTimer();
m_counterTimer = startTimer(1000);
}
void FpsCounter::stopCounterTimer()
{
if (m_counterTimer) {
killTimer(m_counterTimer);
m_counterTimer = 0;
}
}
void FpsCounter::resetCounter()
{
m_rendered = 0;
m_skipped = 0;
}

View file

@ -0,0 +1,38 @@
#ifndef FPSCOUNTER_H
#define FPSCOUNTER_H
#include <QObject>
class FpsCounter : public QObject
{
Q_OBJECT
public:
FpsCounter(QObject *parent = Q_NULLPTR);
virtual ~FpsCounter();
void start();
void stop();
bool isStarted();
void addRenderedFrame();
void addSkippedFrame();
signals:
void updateFPS(quint32 fps);
protected:
virtual void timerEvent(QTimerEvent *event);
private:
void startCounterTimer();
void stopCounterTimer();
void resetCounter();
private:
qint32 m_counterTimer = 0;
quint32 m_curRendered = 0;
quint32 m_curSkipped = 0;
quint32 m_rendered = 0;
quint32 m_skipped = 0;
};
#endif // FPSCOUNTER_H

View file

@ -0,0 +1,128 @@
#include "videobuffer.h"
extern "C"
{
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
}
VideoBuffer::VideoBuffer() {}
VideoBuffer::~VideoBuffer() {}
bool VideoBuffer::init(bool renderExpiredFrames)
{
m_renderExpiredFrames = renderExpiredFrames;
m_decodingFrame = av_frame_alloc();
if (!m_decodingFrame) {
goto error;
}
m_renderingframe = av_frame_alloc();
if (!m_renderingframe) {
goto error;
}
// there is initially no rendering frame, so consider it has already been
// consumed
m_renderingFrameConsumed = true;
m_fpsCounter.start();
return true;
error:
deInit();
return false;
}
void VideoBuffer::deInit()
{
if (m_decodingFrame) {
av_frame_free(&m_decodingFrame);
m_decodingFrame = Q_NULLPTR;
}
if (m_renderingframe) {
av_frame_free(&m_renderingframe);
m_renderingframe = Q_NULLPTR;
}
m_fpsCounter.stop();
}
void VideoBuffer::lock()
{
m_mutex.lock();
}
void VideoBuffer::unLock()
{
m_mutex.unlock();
}
AVFrame *VideoBuffer::decodingFrame()
{
return m_decodingFrame;
}
void VideoBuffer::offerDecodedFrame(bool &previousFrameSkipped)
{
m_mutex.lock();
if (m_renderExpiredFrames) {
// if m_renderExpiredFrames is enable, then the decoder must wait for the current
// frame to be consumed
while (!m_renderingFrameConsumed && !m_interrupted) {
m_renderingFrameConsumedCond.wait(&m_mutex);
}
} else {
if (m_fpsCounter.isStarted() && !m_renderingFrameConsumed) {
m_fpsCounter.addSkippedFrame();
}
}
swap();
previousFrameSkipped = !m_renderingFrameConsumed;
m_renderingFrameConsumed = false;
m_mutex.unlock();
}
const AVFrame *VideoBuffer::consumeRenderedFrame()
{
Q_ASSERT(!m_renderingFrameConsumed);
m_renderingFrameConsumed = true;
if (m_fpsCounter.isStarted()) {
m_fpsCounter.addRenderedFrame();
}
if (m_renderExpiredFrames) {
// if m_renderExpiredFrames is enable, then notify the decoder the current frame is
// consumed, so that it may push a new one
m_renderingFrameConsumedCond.wakeOne();
}
return m_renderingframe;
}
const AVFrame *VideoBuffer::peekRenderedFrame()
{
return m_renderingframe;
}
void VideoBuffer::interrupt()
{
if (m_renderExpiredFrames) {
m_mutex.lock();
m_interrupted = true;
m_mutex.unlock();
// wake up blocking wait
m_renderingFrameConsumedCond.wakeOne();
}
}
FpsCounter *VideoBuffer::getFPSCounter()
{
return &m_fpsCounter;
}
void VideoBuffer::swap()
{
AVFrame *tmp = m_decodingFrame;
m_decodingFrame = m_renderingframe;
m_renderingframe = tmp;
}

View file

@ -0,0 +1,60 @@
#ifndef VIDEO_BUFFER_H
#define VIDEO_BUFFER_H
#include <QMutex>
#include <QWaitCondition>
#include "fpscounter.h"
// forward declarations
typedef struct AVFrame AVFrame;
class VideoBuffer
{
public:
VideoBuffer();
virtual ~VideoBuffer();
bool init(bool renderExpiredFrames = false);
void deInit();
void lock();
void unLock();
AVFrame *decodingFrame();
// set the decoder frame as ready for rendering
// this function locks m_mutex during its execution
// returns true if the previous frame had been consumed
void offerDecodedFrame(bool &previousFrameSkipped);
// mark the rendering frame as consumed and return it
// MUST be called with m_mutex locked!!!
// the caller is expected to render the returned frame to some texture before
// unlocking m_mutex
const AVFrame *consumeRenderedFrame();
const AVFrame *peekRenderedFrame();
// wake up and avoid any blocking call
void interrupt();
FpsCounter *getFPSCounter();
private:
void swap();
private:
AVFrame *m_decodingFrame = Q_NULLPTR;
AVFrame *m_renderingframe = Q_NULLPTR;
QMutex m_mutex;
bool m_renderingFrameConsumed = true;
FpsCounter m_fpsCounter;
bool m_renderExpiredFrames = false;
QWaitCondition m_renderingFrameConsumedCond;
// interrupted is not used if expired frames are not rendered
// since offering a frame will never block
bool m_interrupted = false;
};
#endif // VIDEO_BUFFER_H

389
QtScrcpy/device/device.cpp Normal file
View file

@ -0,0 +1,389 @@
#include <QDir>
#include <QMessageBox>
#include <QTimer>
#include "avframeconvert.h"
#include "config.h"
#include "controller.h"
#include "decoder.h"
#include "device.h"
#include "filehandler.h"
#include "mousetap/mousetap.h"
#include "recorder.h"
#include "server.h"
#include "stream.h"
#include "videobuffer.h"
#include "videoform.h"
extern "C"
{
#include "libavutil/imgutils.h"
}
Device::Device(DeviceParams params, QObject *parent) : QObject(parent), m_params(params)
{
if (!params.display && m_params.recordFileName.trimmed().isEmpty()) {
qCritical("not display must be recorded");
deleteLater();
return;
}
if (params.display) {
m_vb = new VideoBuffer();
m_vb->init(params.renderExpiredFrames);
m_decoder = new Decoder(m_vb, this);
m_fileHandler = new FileHandler(this);
m_controller = new Controller(params.gameScript, this);
m_videoForm = new VideoForm(Config::getInstance().getFramelessWindow(), Config::getInstance().getSkin());
m_videoForm->setDevice(this);
}
m_stream = new Stream(this);
if (m_decoder) {
m_stream->setDecoder(m_decoder);
}
m_server = new Server(this);
if (!m_params.recordFileName.trimmed().isEmpty()) {
m_recorder = new Recorder(m_params.recordFileName);
m_stream->setRecoder(m_recorder);
}
initSignals();
startServer();
}
Device::~Device()
{
if (m_server) {
m_server->stop();
}
// server must stop before decoder, because decoder block main thread
if (m_stream) {
m_stream->stopDecode();
}
if (m_recorder) {
delete m_recorder;
}
if (m_vb) {
m_vb->deInit();
delete m_vb;
}
if (m_videoForm) {
m_videoForm->close();
delete m_videoForm;
}
emit deviceDisconnect(m_params.serial);
}
VideoForm *Device::getVideoForm()
{
return m_videoForm;
}
Server *Device::getServer()
{
return m_server;
}
const QString &Device::getSerial()
{
return m_params.serial;
}
const QSize Device::frameSize()
{
QSize size;
if (!m_videoForm) {
return size;
}
return m_videoForm->frameSize();
}
void Device::updateScript(QString script)
{
if (m_controller) {
m_controller->updateScript(script);
}
}
void Device::onScreenshot()
{
if (!m_vb) {
return;
}
m_vb->lock();
// screenshot
saveFrame(m_vb->peekRenderedFrame());
m_vb->unLock();
}
void Device::onShowTouch(bool show)
{
AdbProcess *adb = new AdbProcess();
if (!adb) {
return;
}
connect(adb, &AdbProcess::adbProcessResult, this, [this](AdbProcess::ADB_EXEC_RESULT processResult) {
if (AdbProcess::AER_SUCCESS_START != processResult) {
sender()->deleteLater();
}
});
adb->setShowTouchesEnabled(getSerial(), show);
qInfo() << getSerial() << " show touch " << (show ? "enable" : "disable");
}
void Device::initSignals()
{
connect(this, &Device::screenshot, this, &Device::onScreenshot);
connect(this, &Device::showTouch, this, &Device::onShowTouch);
connect(this, &Device::setControlState, this, &Device::onSetControlState);
connect(this, &Device::grabCursor, this, &Device::onGrabCursor);
if (m_controller) {
connect(m_controller, &Controller::grabCursor, this, &Device::grabCursor);
}
if (m_controller) {
connect(this, &Device::postGoBack, m_controller, &Controller::onPostGoBack);
connect(this, &Device::postGoHome, m_controller, &Controller::onPostGoHome);
connect(this, &Device::postGoMenu, m_controller, &Controller::onPostGoMenu);
connect(this, &Device::postAppSwitch, m_controller, &Controller::onPostAppSwitch);
connect(this, &Device::postPower, m_controller, &Controller::onPostPower);
connect(this, &Device::postVolumeUp, m_controller, &Controller::onPostVolumeUp);
connect(this, &Device::postVolumeDown, m_controller, &Controller::onPostVolumeDown);
connect(this, &Device::setScreenPowerMode, m_controller, &Controller::onSetScreenPowerMode);
connect(this, &Device::expandNotificationPanel, m_controller, &Controller::onExpandNotificationPanel);
connect(this, &Device::collapseNotificationPanel, m_controller, &Controller::onCollapseNotificationPanel);
connect(this, &Device::mouseEvent, m_controller, &Controller::onMouseEvent);
connect(this, &Device::wheelEvent, m_controller, &Controller::onWheelEvent);
connect(this, &Device::keyEvent, m_controller, &Controller::onKeyEvent);
connect(this, &Device::postBackOrScreenOn, m_controller, &Controller::onPostBackOrScreenOn);
connect(this, &Device::requestDeviceClipboard, m_controller, &Controller::onRequestDeviceClipboard);
connect(this, &Device::setDeviceClipboard, m_controller, &Controller::onSetDeviceClipboard);
connect(this, &Device::clipboardPaste, m_controller, &Controller::onClipboardPaste);
connect(this, &Device::postTextInput, m_controller, &Controller::onPostTextInput);
}
if (m_videoForm) {
connect(m_videoForm, &VideoForm::destroyed, this, [this](QObject *obj) {
Q_UNUSED(obj)
deleteLater();
});
connect(this, &Device::switchFullScreen, m_videoForm, &VideoForm::onSwitchFullScreen);
}
if (m_fileHandler) {
connect(this, &Device::pushFileRequest, m_fileHandler, &FileHandler::onPushFileRequest);
connect(this, &Device::installApkRequest, m_fileHandler, &FileHandler::onInstallApkRequest);
connect(m_fileHandler, &FileHandler::fileHandlerResult, this, [this](FileHandler::FILE_HANDLER_RESULT processResult, bool isApk) {
QString tipsType = "";
if (isApk) {
tipsType = tr("install apk");
} else {
tipsType = tr("file transfer");
}
QString tips;
if (FileHandler::FAR_IS_RUNNING == processResult && m_videoForm) {
tips = tr("wait current %1 to complete").arg(tipsType);
}
if (FileHandler::FAR_SUCCESS_EXEC == processResult && m_videoForm) {
tips = tr("%1 complete, save in %2").arg(tipsType).arg(Config::getInstance().getPushFilePath());
}
if (FileHandler::FAR_ERROR_EXEC == processResult && m_videoForm) {
tips = tr("%1 failed").arg(tipsType);
}
qInfo() << tips;
if (m_controlState == GCS_CLIENT) {
return;
}
QMessageBox::information(m_videoForm, "QtScrcpy", tips, QMessageBox::Ok);
});
}
if (m_server) {
connect(m_server, &Server::serverStartResult, this, [this](bool success) {
if (success) {
m_server->connectTo();
} else {
deleteLater();
}
});
connect(m_server, &Server::connectToResult, this, [this](bool success, const QString &deviceName, const QSize &size) {
if (success) {
double diff = m_startTimeCount.elapsed() / 1000.0;
qInfo() << QString("server start finish in %1s").arg(diff).toStdString().c_str();
// update ui
if (m_videoForm) {
// must be show before updateShowSize
m_videoForm->show();
m_videoForm->setWindowTitle(deviceName);
m_videoForm->updateShowSize(size);
bool deviceVer = size.height() > size.width();
QRect rc = Config::getInstance().getRect(getSerial());
bool rcVer = rc.height() > rc.width();
// same width/height rate
if (rc.isValid() && (deviceVer == rcVer)) {
// mark: resize is for fix setGeometry magneticwidget bug
m_videoForm->resize(rc.size());
m_videoForm->setGeometry(rc);
}
}
// init recorder
if (m_recorder) {
m_recorder->setFrameSize(size);
}
// init decoder
m_stream->setVideoSocket(m_server->getVideoSocket());
m_stream->startDecode();
// init controller
if (m_controller) {
m_controller->setControlSocket(m_server->getControlSocket());
}
// 显示界面时才自动息屏m_params.display
if (m_params.closeScreen && m_params.display && m_controller) {
emit m_controller->onSetScreenPowerMode(ControlMsg::SPM_OFF);
}
}
});
connect(m_server, &Server::onServerStop, this, [this]() {
deleteLater();
qDebug() << "server process stop";
});
}
if (m_stream) {
connect(m_stream, &Stream::onStreamStop, this, [this]() {
deleteLater();
qDebug() << "stream thread stop";
});
}
if (m_decoder && m_vb) {
// must be Qt::QueuedConnection, ui update must be main thread
connect(
m_decoder,
&Decoder::onNewFrame,
this,
[this]() {
m_vb->lock();
const AVFrame *frame = m_vb->consumeRenderedFrame();
if (m_videoForm) {
m_videoForm->updateRender(frame);
}
m_vb->unLock();
},
Qt::QueuedConnection);
connect(m_vb->getFPSCounter(), &::FpsCounter::updateFPS, m_videoForm, &VideoForm::updateFPS);
}
}
void Device::startServer()
{
// fix: macos cant recv finished signel, timer is ok
QTimer::singleShot(0, this, [this]() {
m_startTimeCount.start();
// max size support 480p 720p 1080p 设备原生分辨率
// support wireless connect, example:
//m_server->start("192.168.0.174:5555", 27183, m_maxSize, m_bitRate, "");
// only one devices, serial can be null
// mark: crop input format: "width:height:x:y" or - for no crop, for example: "100:200:0:0"
Server::ServerParams params;
params.serial = m_params.serial;
params.localPort = m_params.localPort;
params.maxSize = m_params.maxSize;
params.bitRate = m_params.bitRate;
params.maxFps = m_params.maxFps;
params.crop = "-";
params.control = true;
params.useReverse = m_params.useReverse;
m_server->start(params);
});
}
void Device::onSetControlState(Device *device, Device::GroupControlState state)
{
Q_UNUSED(device)
if (m_controlState == state) {
return;
}
GroupControlState oldState = m_controlState;
m_controlState = state;
emit controlStateChange(this, oldState, m_controlState);
}
void Device::onGrabCursor(bool grab)
{
if (!m_videoForm) {
return;
}
if (m_controlState == GCS_CLIENT) {
return;
}
QRect rc = m_videoForm->getGrabCursorRect();
MouseTap::getInstance()->enableMouseEventTap(rc, grab);
}
Device::GroupControlState Device::controlState()
{
return m_controlState;
}
bool Device::saveFrame(const AVFrame *frame)
{
if (!frame) {
return false;
}
// create buffer
QImage rgbImage(frame->width, frame->height, QImage::Format_RGB32);
AVFrame *rgbFrame = av_frame_alloc();
if (!rgbFrame) {
return false;
}
// bind buffer to AVFrame
av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, rgbImage.bits(), AV_PIX_FMT_RGB32, frame->width, frame->height, 4);
// convert
AVFrameConvert convert;
convert.setSrcFrameInfo(frame->width, frame->height, AV_PIX_FMT_YUV420P);
convert.setDstFrameInfo(frame->width, frame->height, AV_PIX_FMT_RGB32);
bool ret = false;
ret = convert.init();
if (!ret) {
return false;
}
ret = convert.convert(frame, rgbFrame);
if (!ret) {
return false;
}
convert.deInit();
av_free(rgbFrame);
// save
QString absFilePath;
QString fileDir(Config::getInstance().getRecordPath());
if (fileDir.isEmpty()) {
qWarning() << "please select record save path!!!";
return false;
}
QDateTime dateTime = QDateTime::currentDateTime();
QString fileName = dateTime.toString("_yyyyMMdd_hhmmss_zzz");
fileName = Config::getInstance().getTitle() + fileName + ".png";
QDir dir(fileDir);
absFilePath = dir.absoluteFilePath(fileName);
ret = rgbImage.save(absFilePath, "PNG", 100);
if (!ret) {
return false;
}
qInfo() << "screenshot save to " << absFilePath;
return true;
}

123
QtScrcpy/device/device.h Normal file
View file

@ -0,0 +1,123 @@
#ifndef DEVICE_H
#define DEVICE_H
#include <QPointer>
#include <QTime>
#include "controlmsg.h"
class QMouseEvent;
class QWheelEvent;
class QKeyEvent;
class Recorder;
class Server;
class VideoBuffer;
class Decoder;
class FileHandler;
class Stream;
class VideoForm;
class Controller;
struct AVFrame;
class Device : public QObject
{
Q_OBJECT
public:
struct DeviceParams
{
QString recordFileName = ""; // 视频录制文件名
QString serial = ""; // 设备序列号
quint16 localPort = 27183; // reverse时本地监听端口
quint16 maxSize = 720; // 视频分辨率
quint32 bitRate = 8000000; // 视频比特率
quint32 maxFps = 60; // 视频最大帧率
bool closeScreen = false; // 启动时自动息屏
bool useReverse = true; // true:先使用adb reverse失败后自动使用adb forwardfalse:直接使用adb forward
bool display = true; // 是否显示画面(或者仅仅后台录制)
QString gameScript = ""; // 游戏映射脚本
bool renderExpiredFrames = false; // 是否渲染延迟视频帧
};
enum GroupControlState
{
GCS_FREE = 0,
GCS_HOST,
GCS_CLIENT,
};
explicit Device(DeviceParams params, QObject *parent = nullptr);
virtual ~Device();
VideoForm *getVideoForm();
Server *getServer();
const QString &getSerial();
const QSize frameSize();
void updateScript(QString script);
Device::GroupControlState controlState();
signals:
void deviceDisconnect(QString serial);
// tool bar
void switchFullScreen();
void postGoBack();
void postGoHome();
void postGoMenu();
void postAppSwitch();
void postPower();
void postVolumeUp();
void postVolumeDown();
void setScreenPowerMode(ControlMsg::ScreenPowerMode mode);
void expandNotificationPanel();
void collapseNotificationPanel();
void postBackOrScreenOn();
void postTextInput(QString &text);
void requestDeviceClipboard();
void setDeviceClipboard();
void clipboardPaste();
void pushFileRequest(const QString &serial, const QString &file, const QString &devicePath = "");
void installApkRequest(const QString &serial, const QString &apkFile);
// key map
void mouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize);
void wheelEvent(const QWheelEvent *from, const QSize &frameSize, const QSize &showSize);
void keyEvent(const QKeyEvent *from, const QSize &frameSize, const QSize &showSize);
// self connect signal and slots
void screenshot();
void showTouch(bool show);
void setControlState(Device *device, Device::GroupControlState state);
void grabCursor(bool grab);
// for notify
void controlStateChange(Device *device, Device::GroupControlState oldState, Device::GroupControlState newState);
public slots:
void onScreenshot();
void onShowTouch(bool show);
void onSetControlState(Device *device, Device::GroupControlState state);
void onGrabCursor(bool grab);
private:
void initSignals();
void startServer();
bool saveFrame(const AVFrame *frame);
private:
// server relevant
QPointer<Server> m_server;
QPointer<Decoder> m_decoder;
QPointer<Controller> m_controller;
QPointer<FileHandler> m_fileHandler;
QPointer<Stream> m_stream;
VideoBuffer *m_vb = Q_NULLPTR;
Recorder *m_recorder = Q_NULLPTR;
// ui
QPointer<VideoForm> m_videoForm;
QTime m_startTimeCount;
DeviceParams m_params;
GroupControlState m_controlState = GCS_FREE;
};
#endif // DEVICE_H

View file

@ -0,0 +1,27 @@
HEADERS += \
$$PWD/device.h
SOURCES += \
$$PWD/device.cpp
include ($$PWD/server/server.pri)
include ($$PWD/decoder/decoder.pri)
include ($$PWD/render/render.pri)
include ($$PWD/stream/stream.pri)
include ($$PWD/android/android.pri)
include ($$PWD/controller/controller.pri)
include ($$PWD/filehandler/filehandler.pri)
include ($$PWD/recorder/recorder.pri)
include ($$PWD/ui/ui.pri)
INCLUDEPATH += \
$$PWD/../../third_party/ffmpeg/include \
$$PWD/server \
$$PWD/decoder \
$$PWD/render \
$$PWD/stream \
$$PWD/android \
$$PWD/controller \
$$PWD/filehandler \
$$PWD/recorder \
$$PWD/ui

View file

@ -0,0 +1,42 @@
#include "filehandler.h"
FileHandler::FileHandler(QObject *parent) : QObject(parent)
{
connect(&m_adb, &AdbProcess::adbProcessResult, this, [this](AdbProcess::ADB_EXEC_RESULT processResult) {
switch (processResult) {
case AdbProcess::AER_ERROR_START:
case AdbProcess::AER_ERROR_EXEC:
case AdbProcess::AER_ERROR_MISSING_BINARY:
emit fileHandlerResult(FAR_ERROR_EXEC, m_isApk);
break;
case AdbProcess::AER_SUCCESS_EXEC:
emit fileHandlerResult(FAR_SUCCESS_EXEC, m_isApk);
break;
default:
break;
}
});
}
FileHandler::~FileHandler() {}
void FileHandler::onPushFileRequest(const QString &serial, const QString &file, const QString &devicePath)
{
if (m_adb.isRuning()) {
emit fileHandlerResult(FAR_IS_RUNNING, false);
return;
}
m_isApk = false;
m_adb.push(serial, file, devicePath);
}
void FileHandler::onInstallApkRequest(const QString &serial, const QString &apkFile)
{
if (m_adb.isRuning()) {
emit fileHandlerResult(FAR_IS_RUNNING, true);
return;
}
m_isApk = true;
m_adb.install(serial, apkFile);
}

View file

@ -0,0 +1,36 @@
#ifndef FILEHANDLER_H
#define FILEHANDLER_H
#include <QObject>
#include "adbprocess.h"
class FileHandler : public QObject
{
Q_OBJECT
public:
enum FILE_HANDLER_RESULT
{
FAR_IS_RUNNING, // 正在执行
FAR_SUCCESS_EXEC, // 执行成功
FAR_ERROR_EXEC, // 执行失败
};
FileHandler(QObject *parent = nullptr);
virtual ~FileHandler();
const QString &getDevicePath();
public slots:
void onPushFileRequest(const QString &serial, const QString &file, const QString &devicePath = "");
void onInstallApkRequest(const QString &serial, const QString &apkFile);
signals:
void fileHandlerResult(FILE_HANDLER_RESULT processResult, bool isApk = false);
private:
AdbProcess m_adb;
bool m_isApk = false;
QString m_devicePath = "";
};
#endif // FILEHANDLER_H

View file

@ -0,0 +1,5 @@
HEADERS += \
$$PWD/filehandler.h
SOURCES += \
$$PWD/filehandler.cpp

View file

@ -0,0 +1,330 @@
#include <QCoreApplication>
#include <QDebug>
#include <QFileInfo>
#include "compat.h"
#include "recorder.h"
static const AVRational SCRCPY_TIME_BASE = { 1, 1000000 }; // timestamps in us
Recorder::Recorder(const QString &fileName, QObject *parent) : QThread(parent), m_fileName(fileName), m_format(guessRecordFormat(fileName)) {}
Recorder::~Recorder() {}
AVPacket *Recorder::packetNew(const AVPacket *packet)
{
AVPacket *rec = new AVPacket;
if (!rec) {
return Q_NULLPTR;
}
// av_packet_ref() does not initialize all fields in old FFmpeg versions
av_init_packet(rec);
if (av_packet_ref(rec, packet)) {
delete rec;
return Q_NULLPTR;
}
return rec;
}
void Recorder::packetDelete(AVPacket *packet)
{
av_packet_unref(packet);
delete packet;
}
void Recorder::queueClear()
{
while (!m_queue.isEmpty()) {
packetDelete(m_queue.dequeue());
}
}
void Recorder::setFrameSize(const QSize &declaredFrameSize)
{
m_declaredFrameSize = declaredFrameSize;
}
void Recorder::setFormat(Recorder::RecorderFormat format)
{
m_format = format;
}
bool Recorder::open(const AVCodec *inputCodec)
{
QString formatName = recorderGetFormatName(m_format);
Q_ASSERT(!formatName.isEmpty());
const AVOutputFormat *format = findMuxer(formatName.toUtf8());
if (!format) {
qCritical("Could not find muxer");
return false;
}
m_formatCtx = avformat_alloc_context();
if (!m_formatCtx) {
qCritical("Could not allocate output context");
return false;
}
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
// returns (on purpose) a pointer-to-const, but AVFormatContext.oformat
// still expects a pointer-to-non-const (it has not be updated accordingly)
// <https://github.com/FFmpeg/FFmpeg/commit/0694d8702421e7aff1340038559c438b61bb30dd>
m_formatCtx->oformat = (AVOutputFormat *)format;
QString comment = "Recorded by QtScrcpy " + QCoreApplication::applicationVersion();
av_dict_set(&m_formatCtx->metadata, "comment", comment.toUtf8(), 0);
AVStream *outStream = avformat_new_stream(m_formatCtx, inputCodec);
if (!outStream) {
avformat_free_context(m_formatCtx);
m_formatCtx = Q_NULLPTR;
return false;
}
#ifdef QTSCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
outStream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
outStream->codecpar->codec_id = inputCodec->id;
outStream->codecpar->format = AV_PIX_FMT_YUV420P;
outStream->codecpar->width = m_declaredFrameSize.width();
outStream->codecpar->height = m_declaredFrameSize.height();
#else
outStream->codec->codec_type = AVMEDIA_TYPE_VIDEO;
outStream->codec->codec_id = inputCodec->id;
outStream->codec->pix_fmt = AV_PIX_FMT_YUV420P;
outStream->codec->width = m_declaredFrameSize.width();
outStream->codec->height = m_declaredFrameSize.height();
#endif
int ret = avio_open(&m_formatCtx->pb, m_fileName.toUtf8().toStdString().c_str(), AVIO_FLAG_WRITE);
if (ret < 0) {
char errorbuf[255] = { 0 };
av_strerror(ret, errorbuf, 254);
qCritical() << QString("Failed to open output file: %1 %2").arg(errorbuf).arg(m_fileName).toUtf8().toStdString().c_str();
// ostream will be cleaned up during context cleaning
avformat_free_context(m_formatCtx);
m_formatCtx = Q_NULLPTR;
return false;
}
return true;
}
void Recorder::close()
{
if (Q_NULLPTR != m_formatCtx) {
if (m_headerWritten) {
int ret = av_write_trailer(m_formatCtx);
if (ret < 0) {
qCritical() << QString("Failed to write trailer to %1").arg(m_fileName).toUtf8().toStdString().c_str();
m_failed = true;
} else {
qInfo() << QString("success record %1").arg(m_fileName).toStdString().c_str();
}
} else {
// the recorded file is empty
m_failed = true;
}
avio_close(m_formatCtx->pb);
avformat_free_context(m_formatCtx);
m_formatCtx = Q_NULLPTR;
}
}
bool Recorder::write(AVPacket *packet)
{
if (!m_headerWritten) {
if (packet->pts != AV_NOPTS_VALUE) {
qCritical("The first packet is not a config packet");
return false;
}
bool ok = recorderWriteHeader(packet);
if (!ok) {
return false;
}
m_headerWritten = true;
return true;
}
if (packet->pts == AV_NOPTS_VALUE) {
// ignore config packets
return true;
}
recorderRescalePacket(packet);
return av_write_frame(m_formatCtx, packet) >= 0;
}
const AVOutputFormat *Recorder::findMuxer(const char *name)
{
#ifdef QTSCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
void *opaque = Q_NULLPTR;
#endif
const AVOutputFormat *outFormat = Q_NULLPTR;
do {
#ifdef QTSCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
outFormat = av_muxer_iterate(&opaque);
#else
outFormat = av_oformat_next(outFormat);
#endif
// until null or with name "name"
} while (outFormat && strcmp(outFormat->name, name));
return outFormat;
}
bool Recorder::recorderWriteHeader(const AVPacket *packet)
{
AVStream *ostream = m_formatCtx->streams[0];
quint8 *extradata = (quint8 *)av_malloc(packet->size * sizeof(quint8));
if (!extradata) {
qCritical("Cannot allocate extradata");
return false;
}
// copy the first packet to the extra data
memcpy(extradata, packet->data, packet->size);
#ifdef QTSCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
ostream->codecpar->extradata = extradata;
ostream->codecpar->extradata_size = packet->size;
#else
ostream->codec->extradata = extradata;
ostream->codec->extradata_size = packet->size;
#endif
int ret = avformat_write_header(m_formatCtx, NULL);
if (ret < 0) {
qCritical("Failed to write header recorder file");
return false;
}
return true;
}
void Recorder::recorderRescalePacket(AVPacket *packet)
{
AVStream *ostream = m_formatCtx->streams[0];
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
}
QString Recorder::recorderGetFormatName(Recorder::RecorderFormat format)
{
switch (format) {
case RECORDER_FORMAT_MP4:
return "mp4";
case RECORDER_FORMAT_MKV:
return "matroska";
default:
return "";
}
}
Recorder::RecorderFormat Recorder::guessRecordFormat(const QString &fileName)
{
if (4 > fileName.length()) {
return Recorder::RECORDER_FORMAT_NULL;
}
QFileInfo fileInfo = QFileInfo(fileName);
QString ext = fileInfo.suffix();
if (0 == ext.compare("mp4")) {
return Recorder::RECORDER_FORMAT_MP4;
}
if (0 == ext.compare("mkv")) {
return Recorder::RECORDER_FORMAT_MKV;
}
return Recorder::RECORDER_FORMAT_NULL;
}
void Recorder::run()
{
for (;;) {
AVPacket *rec = Q_NULLPTR;
{
QMutexLocker locker(&m_mutex);
while (!m_stopped && m_queue.isEmpty()) {
m_recvDataCond.wait(&m_mutex);
}
// if stopped is set, continue to process the remaining events (to
// finish the recording) before actually stopping
if (m_stopped && m_queue.isEmpty()) {
AVPacket *last = m_previous;
if (last) {
// assign an arbitrary duration to the last packet
last->duration = 100000;
bool ok = write(last);
if (!ok) {
// failing to write the last frame is not very serious, no
// future frame may depend on it, so the resulting file
// will still be valid
qWarning("Could not record last packet");
}
packetDelete(last);
}
break;
}
rec = m_queue.dequeue();
}
// recorder->previous is only written from this thread, no need to lock
AVPacket *previous = m_previous;
m_previous = rec;
if (!previous) {
// we just received the first packet
continue;
}
// config packets have no PTS, we must ignore them
if (rec->pts != AV_NOPTS_VALUE && previous->pts != AV_NOPTS_VALUE) {
// we now know the duration of the previous packet
previous->duration = rec->pts - previous->pts;
}
bool ok = write(previous);
packetDelete(previous);
if (!ok) {
qCritical("Could not record packet");
QMutexLocker locker(&m_mutex);
m_failed = true;
// discard pending packets
queueClear();
break;
}
}
qDebug("Recorder thread ended");
}
bool Recorder::startRecorder()
{
start();
return true;
}
void Recorder::stopRecorder()
{
QMutexLocker locker(&m_mutex);
m_stopped = true;
m_recvDataCond.wakeOne();
}
bool Recorder::push(const AVPacket *packet)
{
QMutexLocker locker(&m_mutex);
Q_ASSERT(!m_stopped);
if (m_failed) {
// reject any new packet (this will stop the stream)
return false;
}
AVPacket *rec = packetNew(packet);
if (rec) {
m_queue.enqueue(rec);
m_recvDataCond.wakeOne();
}
return rec != Q_NULLPTR;
}

View file

@ -0,0 +1,71 @@
#ifndef RECORDER_H
#define RECORDER_H
#include <QMutex>
#include <QQueue>
#include <QSize>
#include <QString>
#include <QThread>
#include <QWaitCondition>
extern "C"
{
#include "libavformat/avformat.h"
}
class Recorder : public QThread
{
Q_OBJECT
public:
enum RecorderFormat
{
RECORDER_FORMAT_NULL = 0,
RECORDER_FORMAT_MP4,
RECORDER_FORMAT_MKV,
};
Recorder(const QString &fileName, QObject *parent = Q_NULLPTR);
virtual ~Recorder();
void setFrameSize(const QSize &declaredFrameSize);
void setFormat(Recorder::RecorderFormat format);
bool open(const AVCodec *inputCodec);
void close();
bool write(AVPacket *packet);
bool startRecorder();
void stopRecorder();
bool push(const AVPacket *packet);
private:
const AVOutputFormat *findMuxer(const char *name);
bool recorderWriteHeader(const AVPacket *packet);
void recorderRescalePacket(AVPacket *packet);
QString recorderGetFormatName(Recorder::RecorderFormat format);
RecorderFormat guessRecordFormat(const QString &fileName);
private:
AVPacket *packetNew(const AVPacket *packet);
void packetDelete(AVPacket *packet);
void queueClear();
protected:
void run();
private:
QString m_fileName = "";
AVFormatContext *m_formatCtx = Q_NULLPTR;
QSize m_declaredFrameSize;
bool m_headerWritten = false;
RecorderFormat m_format = RECORDER_FORMAT_NULL;
QMutex m_mutex;
QWaitCondition m_recvDataCond;
bool m_stopped = false; // set on recorder_stop() by the stream reader
bool m_failed = false; // set on packet write failure
QQueue<AVPacket *> m_queue;
// we can write a packet only once we received the next one so that we can
// set its duration (next_pts - current_pts)
// "previous" is only accessed from the recorder thread, so it does not
// need to be protected by the mutex
AVPacket *m_previous = Q_NULLPTR;
};
#endif // RECORDER_H

View file

@ -0,0 +1,5 @@
HEADERS += \
$$PWD/recorder.h
SOURCES += \
$$PWD/recorder.cpp

View file

@ -157,8 +157,6 @@ void QYUVOpenGLWidget::initializeGL()
void QYUVOpenGLWidget::paintGL()
{
m_shaderProgram.bind();
if (m_needUpdate) {
deInitTextures();
initTextures();
@ -177,8 +175,6 @@ void QYUVOpenGLWidget::paintGL()
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
m_shaderProgram.release();
}
void QYUVOpenGLWidget::resizeGL(int width, int height)

View file

@ -0,0 +1,5 @@
HEADERS += \
$$PWD/qyuvopenglwidget.h
SOURCES += \
$$PWD/qyuvopenglwidget.cpp

View file

@ -0,0 +1,483 @@
#include <QCoreApplication>
#include <QDebug>
#include <QFileInfo>
#include <QThread>
#include <QTimer>
#include <QTimerEvent>
#include "config.h"
#include "server.h"
#define DEVICE_NAME_FIELD_LENGTH 64
#define SOCKET_NAME "scrcpy"
#define MAX_CONNECT_COUNT 30
#define MAX_RESTART_COUNT 1
Server::Server(QObject *parent) : QObject(parent)
{
connect(&m_workProcess, &AdbProcess::adbProcessResult, this, &Server::onWorkProcessResult);
connect(&m_serverProcess, &AdbProcess::adbProcessResult, this, &Server::onWorkProcessResult);
connect(&m_serverSocket, &QTcpServer::newConnection, this, [this]() {
QTcpSocket *tmp = m_serverSocket.nextPendingConnection();
if (dynamic_cast<VideoSocket *>(tmp)) {
m_videoSocket = dynamic_cast<VideoSocket *>(tmp);
if (!m_videoSocket->isValid() || !readInfo(m_videoSocket, m_deviceName, m_deviceSize)) {
stop();
emit connectToResult(false);
}
} else {
m_controlSocket = tmp;
if (m_controlSocket && m_controlSocket->isValid()) {
// we don't need the server socket anymore
// just m_videoSocket is ok
m_serverSocket.close();
// we don't need the adb tunnel anymore
disableTunnelReverse();
m_tunnelEnabled = false;
emit connectToResult(true, m_deviceName, m_deviceSize);
} else {
stop();
emit connectToResult(false);
}
stopAcceptTimeoutTimer();
}
});
}
Server::~Server() {}
const QString &Server::getServerPath()
{
if (m_serverPath.isEmpty()) {
m_serverPath = QString::fromLocal8Bit(qgetenv("QTSCRCPY_SERVER_PATH"));
QFileInfo fileInfo(m_serverPath);
if (m_serverPath.isEmpty() || !fileInfo.isFile()) {
m_serverPath = QCoreApplication::applicationDirPath() + "/scrcpy-server";
}
}
return m_serverPath;
}
bool Server::pushServer()
{
if (m_workProcess.isRuning()) {
m_workProcess.kill();
}
m_workProcess.push(m_params.serial, getServerPath(), Config::getInstance().getServerPath());
return true;
}
bool Server::enableTunnelReverse()
{
if (m_workProcess.isRuning()) {
m_workProcess.kill();
}
m_workProcess.reverse(m_params.serial, SOCKET_NAME, m_params.localPort);
return true;
}
bool Server::disableTunnelReverse()
{
AdbProcess *adb = new AdbProcess();
if (!adb) {
return false;
}
connect(adb, &AdbProcess::adbProcessResult, this, [this](AdbProcess::ADB_EXEC_RESULT processResult) {
if (AdbProcess::AER_SUCCESS_START != processResult) {
sender()->deleteLater();
}
});
adb->reverseRemove(m_params.serial, SOCKET_NAME);
return true;
}
bool Server::enableTunnelForward()
{
if (m_workProcess.isRuning()) {
m_workProcess.kill();
}
m_workProcess.forward(m_params.serial, m_params.localPort, SOCKET_NAME);
return true;
}
bool Server::disableTunnelForward()
{
AdbProcess *adb = new AdbProcess();
if (!adb) {
return false;
}
connect(adb, &AdbProcess::adbProcessResult, this, [this](AdbProcess::ADB_EXEC_RESULT processResult) {
if (AdbProcess::AER_SUCCESS_START != processResult) {
sender()->deleteLater();
}
});
adb->forwardRemove(m_params.serial, m_params.localPort);
return true;
}
bool Server::execute()
{
if (m_serverProcess.isRuning()) {
m_serverProcess.kill();
}
QStringList args;
args << "shell";
args << QString("CLASSPATH=%1").arg(Config::getInstance().getServerPath());
args << "app_process";
args << "/"; // unused;
args << "com.genymobile.scrcpy.Server";
args << Config::getInstance().getServerVersion();
args << QString::number(m_params.maxSize);
args << QString::number(m_params.bitRate);
args << QString::number(m_params.maxFps);
args << (m_tunnelForward ? "true" : "false");
if (m_params.crop.isEmpty()) {
args << "-";
} else {
args << m_params.crop;
}
args << "true"; // always send frame meta (packet boundaries + timestamp)
args << (m_params.control ? "true" : "false");
// adb -s P7C0218510000537 shell CLASSPATH=/data/local/tmp/scrcpy-server app_process / com.genymobile.scrcpy.Server 0 8000000 false
// mark: crop input format: "width:height:x:y" or - for no crop, for example: "100:200:0:0"
// 这条adb命令是阻塞运行的m_serverProcess进程不会退出了
m_serverProcess.execute(m_params.serial, args);
return true;
}
bool Server::start(Server::ServerParams params)
{
m_params = params;
m_serverStartStep = SSS_PUSH;
return startServerByStep();
}
bool Server::connectTo()
{
if (SSS_RUNNING != m_serverStartStep) {
qWarning("server not run");
return false;
}
if (!m_tunnelForward && !m_videoSocket) {
startAcceptTimeoutTimer();
return true;
}
startConnectTimeoutTimer();
return true;
}
bool Server::isReverse()
{
return !m_tunnelForward;
}
Server::ServerParams Server::getParams()
{
return m_params;
}
void Server::timerEvent(QTimerEvent *event)
{
if (event && m_acceptTimeoutTimer == event->timerId()) {
stopAcceptTimeoutTimer();
emit connectToResult(false, "", QSize());
} else if (event && m_connectTimeoutTimer == event->timerId()) {
onConnectTimer();
}
}
VideoSocket *Server::getVideoSocket()
{
return m_videoSocket;
}
QTcpSocket *Server::getControlSocket()
{
return m_controlSocket;
}
void Server::stop()
{
if (m_tunnelForward) {
stopConnectTimeoutTimer();
} else {
stopAcceptTimeoutTimer();
}
if (m_videoSocket) {
m_videoSocket->close();
m_videoSocket->deleteLater();
}
if (m_controlSocket) {
m_controlSocket->close();
m_controlSocket->deleteLater();
}
// ignore failure
m_serverProcess.kill();
if (m_tunnelEnabled) {
if (m_tunnelForward) {
disableTunnelForward();
} else {
disableTunnelReverse();
}
m_tunnelForward = false;
m_tunnelEnabled = false;
}
m_serverSocket.close();
}
bool Server::startServerByStep()
{
bool stepSuccess = false;
// push, enable tunnel et start the server
if (SSS_NULL != m_serverStartStep) {
switch (m_serverStartStep) {
case SSS_PUSH:
stepSuccess = pushServer();
break;
case SSS_ENABLE_TUNNEL_REVERSE:
stepSuccess = enableTunnelReverse();
break;
case SSS_ENABLE_TUNNEL_FORWARD:
stepSuccess = enableTunnelForward();
break;
case SSS_EXECUTE_SERVER:
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
// "adb forward", so the app socket is the client
if (!m_tunnelForward) {
// At the application level, the device part is "the server" because it
// serves video stream and control. However, at the network level, the
// client listens and the server connects to the client. That way, the
// client can listen before starting the server app, so there is no need to
// try to connect until the server socket is listening on the device.
m_serverSocket.setMaxPendingConnections(2);
if (!m_serverSocket.listen(QHostAddress::LocalHost, m_params.localPort)) {
qCritical() << QString("Could not listen on port %1").arg(m_params.localPort).toStdString().c_str();
m_serverStartStep = SSS_NULL;
if (m_tunnelForward) {
disableTunnelForward();
} else {
disableTunnelReverse();
}
emit serverStartResult(false);
return false;
}
}
// server will connect to our server socket
stepSuccess = execute();
break;
default:
break;
}
}
if (!stepSuccess) {
emit serverStartResult(false);
}
return stepSuccess;
}
bool Server::readInfo(VideoSocket *videoSocket, QString &deviceName, QSize &size)
{
unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4];
if (videoSocket->bytesAvailable() <= (DEVICE_NAME_FIELD_LENGTH + 4)) {
videoSocket->waitForReadyRead(300);
}
qint64 len = videoSocket->read((char *)buf, sizeof(buf));
if (len < DEVICE_NAME_FIELD_LENGTH + 4) {
qInfo("Could not retrieve device information");
return false;
}
buf[DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; // in case the client sends garbage
// strcpy is safe here, since name contains at least DEVICE_NAME_FIELD_LENGTH bytes
// and strlen(buf) < DEVICE_NAME_FIELD_LENGTH
deviceName = (char *)buf;
size.setWidth((buf[DEVICE_NAME_FIELD_LENGTH] << 8) | buf[DEVICE_NAME_FIELD_LENGTH + 1]);
size.setHeight((buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8) | buf[DEVICE_NAME_FIELD_LENGTH + 3]);
return true;
}
void Server::startAcceptTimeoutTimer()
{
stopAcceptTimeoutTimer();
m_acceptTimeoutTimer = startTimer(1000);
}
void Server::stopAcceptTimeoutTimer()
{
if (m_acceptTimeoutTimer) {
killTimer(m_acceptTimeoutTimer);
m_acceptTimeoutTimer = 0;
}
}
void Server::startConnectTimeoutTimer()
{
stopConnectTimeoutTimer();
m_connectTimeoutTimer = startTimer(100);
}
void Server::stopConnectTimeoutTimer()
{
if (m_connectTimeoutTimer) {
killTimer(m_connectTimeoutTimer);
m_connectTimeoutTimer = 0;
}
m_connectCount = 0;
}
void Server::onConnectTimer()
{
// device server need time to start
// 这里连接太早时间不够导致安卓监听socket还没有建立readInfo会失败所以采取定时重试策略
// 每隔100ms尝试一次最多尝试MAX_CONNECT_COUNT次
QString deviceName;
QSize deviceSize;
bool success = false;
VideoSocket *videoSocket = new VideoSocket();
QTcpSocket *controlSocket = new QTcpSocket();
videoSocket->connectToHost(QHostAddress::LocalHost, m_params.localPort);
if (!videoSocket->waitForConnected(1000)) {
// 连接到adb很快的这里失败不重试
m_connectCount = MAX_CONNECT_COUNT;
qWarning("video socket connect to server failed");
goto result;
}
controlSocket->connectToHost(QHostAddress::LocalHost, m_params.localPort);
if (!controlSocket->waitForConnected(1000)) {
// 连接到adb很快的这里失败不重试
m_connectCount = MAX_CONNECT_COUNT;
qWarning("control socket connect to server failed");
goto result;
}
if (QTcpSocket::ConnectedState == videoSocket->state()) {
// connect will success even if devices offline, recv data is real connect success
// because connect is to pc adb server
videoSocket->waitForReadyRead(1000);
// devices will send 1 byte first on tunnel forward mode
QByteArray data = videoSocket->read(1);
if (!data.isEmpty() && readInfo(videoSocket, deviceName, deviceSize)) {
success = true;
goto result;
} else {
qWarning("video socket connect to server read device info failed, try again");
goto result;
}
} else {
qWarning("connect to server failed");
m_connectCount = MAX_CONNECT_COUNT;
goto result;
}
result:
if (success) {
stopConnectTimeoutTimer();
m_videoSocket = videoSocket;
m_controlSocket = controlSocket;
// we don't need the adb tunnel anymore
disableTunnelForward();
m_tunnelEnabled = false;
m_restartCount = 0;
emit connectToResult(success, deviceName, deviceSize);
return;
}
if (videoSocket) {
videoSocket->deleteLater();
}
if (controlSocket) {
controlSocket->deleteLater();
}
if (MAX_CONNECT_COUNT <= m_connectCount++) {
stopConnectTimeoutTimer();
stop();
if (MAX_RESTART_COUNT > m_restartCount++) {
qWarning("restart server auto");
start(m_params);
} else {
m_restartCount = 0;
emit connectToResult(false);
}
}
}
void Server::onWorkProcessResult(AdbProcess::ADB_EXEC_RESULT processResult)
{
if (sender() == &m_workProcess) {
if (SSS_NULL != m_serverStartStep) {
switch (m_serverStartStep) {
case SSS_PUSH:
if (AdbProcess::AER_SUCCESS_EXEC == processResult) {
if (m_params.useReverse) {
m_serverStartStep = SSS_ENABLE_TUNNEL_REVERSE;
} else {
m_tunnelForward = true;
m_serverStartStep = SSS_ENABLE_TUNNEL_FORWARD;
}
startServerByStep();
} else if (AdbProcess::AER_SUCCESS_START != processResult) {
qCritical("adb push failed");
m_serverStartStep = SSS_NULL;
emit serverStartResult(false);
}
break;
case SSS_ENABLE_TUNNEL_REVERSE:
if (AdbProcess::AER_SUCCESS_EXEC == processResult) {
m_serverStartStep = SSS_EXECUTE_SERVER;
startServerByStep();
} else if (AdbProcess::AER_SUCCESS_START != processResult) {
// 有一些设备reverse会报错more than o'ne deviceadb的bug
// https://github.com/Genymobile/scrcpy/issues/5
qCritical("adb reverse failed");
m_tunnelForward = true;
m_serverStartStep = SSS_ENABLE_TUNNEL_FORWARD;
startServerByStep();
}
break;
case SSS_ENABLE_TUNNEL_FORWARD:
if (AdbProcess::AER_SUCCESS_EXEC == processResult) {
m_serverStartStep = SSS_EXECUTE_SERVER;
startServerByStep();
} else if (AdbProcess::AER_SUCCESS_START != processResult) {
qCritical("adb forward failed");
m_serverStartStep = SSS_NULL;
emit serverStartResult(false);
}
break;
default:
break;
}
}
}
if (sender() == &m_serverProcess) {
if (SSS_EXECUTE_SERVER == m_serverStartStep) {
if (AdbProcess::AER_SUCCESS_START == processResult) {
m_serverStartStep = SSS_RUNNING;
m_tunnelEnabled = true;
emit serverStartResult(true);
} else if (AdbProcess::AER_ERROR_START == processResult) {
if (!m_tunnelForward) {
m_serverSocket.close();
disableTunnelReverse();
} else {
disableTunnelForward();
}
qCritical("adb shell start server failed");
m_serverStartStep = SSS_NULL;
emit serverStartResult(false);
}
} else if (SSS_RUNNING == m_serverStartStep) {
m_serverStartStep = SSS_NULL;
emit onServerStop();
}
}
}

View file

@ -0,0 +1,99 @@
#ifndef SERVER_H
#define SERVER_H
#include <QObject>
#include <QPointer>
#include <QSize>
#include "adbprocess.h"
#include "tcpserver.h"
#include "videosocket.h"
class Server : public QObject
{
Q_OBJECT
enum SERVER_START_STEP
{
SSS_NULL,
SSS_PUSH,
SSS_ENABLE_TUNNEL_REVERSE,
SSS_ENABLE_TUNNEL_FORWARD,
SSS_EXECUTE_SERVER,
SSS_RUNNING,
};
public:
struct ServerParams
{
QString serial = ""; // 设备序列号
quint16 localPort = 27183; // reverse时本地监听端口
quint16 maxSize = 720; // 视频分辨率
quint32 bitRate = 8000000; // 视频比特率
quint32 maxFps = 60; // 视频最大帧率
QString crop = "-"; // 视频裁剪
bool control = true; // 安卓端是否接收键鼠控制
bool useReverse = true; // true:先使用adb reverse失败后自动使用adb forwardfalse:直接使用adb forward
};
explicit Server(QObject *parent = nullptr);
virtual ~Server();
bool start(Server::ServerParams params);
bool connectTo();
bool isReverse();
Server::ServerParams getParams();
VideoSocket *getVideoSocket();
QTcpSocket *getControlSocket();
void stop();
signals:
void serverStartResult(bool success);
void connectToResult(bool success, const QString &deviceName = "", const QSize &size = QSize());
void onServerStop();
private slots:
void onWorkProcessResult(AdbProcess::ADB_EXEC_RESULT processResult);
protected:
void timerEvent(QTimerEvent *event);
private:
const QString &getServerPath();
bool pushServer();
bool enableTunnelReverse();
bool disableTunnelReverse();
bool enableTunnelForward();
bool disableTunnelForward();
bool execute();
bool startServerByStep();
bool readInfo(VideoSocket *videoSocket, QString &deviceName, QSize &size);
void startAcceptTimeoutTimer();
void stopAcceptTimeoutTimer();
void startConnectTimeoutTimer();
void stopConnectTimeoutTimer();
void onConnectTimer();
private:
QString m_serverPath = "";
AdbProcess m_workProcess;
AdbProcess m_serverProcess;
TcpServer m_serverSocket; // only used if !tunnel_forward
QPointer<VideoSocket> m_videoSocket = Q_NULLPTR;
QPointer<QTcpSocket> m_controlSocket = Q_NULLPTR;
bool m_tunnelEnabled = false;
bool m_tunnelForward = false; // use "adb forward" instead of "adb reverse"
int m_acceptTimeoutTimer = 0;
int m_connectTimeoutTimer = 0;
quint32 m_connectCount = 0;
quint32 m_restartCount = 0;
QString m_deviceName = "";
QSize m_deviceSize = QSize();
ServerParams m_params;
SERVER_START_STEP m_serverStartStep = SSS_NULL;
};
#endif // SERVER_H

View file

@ -0,0 +1,9 @@
HEADERS += \
$$PWD/server.h \
$$PWD/tcpserver.h \
$$PWD/videosocket.h
SOURCES += \
$$PWD/server.cpp \
$$PWD/tcpserver.cpp \
$$PWD/videosocket.cpp

View file

@ -0,0 +1,22 @@
#include "tcpserver.h"
#include "videosocket.h"
TcpServer::TcpServer(QObject *parent) : QTcpServer(parent) {}
TcpServer::~TcpServer() {}
void TcpServer::incomingConnection(qintptr handle)
{
if (m_isVideoSocket) {
VideoSocket *socket = new VideoSocket();
socket->setSocketDescriptor(handle);
addPendingConnection(socket);
// next is control socket
m_isVideoSocket = false;
} else {
QTcpSocket *socket = new QTcpSocket();
socket->setSocketDescriptor(handle);
addPendingConnection(socket);
}
}

View file

@ -0,0 +1,20 @@
#ifndef TCPSERVER_H
#define TCPSERVER_H
#include <QTcpServer>
class TcpServer : public QTcpServer
{
Q_OBJECT
public:
explicit TcpServer(QObject *parent = nullptr);
virtual ~TcpServer();
protected:
virtual void incomingConnection(qintptr handle);
private:
bool m_isVideoSocket = true;
};
#endif // TCPSERVER_H

View file

@ -0,0 +1,81 @@
#include <QCoreApplication>
#include <QDebug>
#include <QThread>
#include "qscrcpyevent.h"
#include "videosocket.h"
VideoSocket::VideoSocket(QObject *parent) : QTcpSocket(parent)
{
connect(this, &VideoSocket::readyRead, this, &VideoSocket::onReadyRead);
connect(this, &VideoSocket::aboutToClose, this, &VideoSocket::quitNotify);
connect(this, &VideoSocket::disconnected, this, &VideoSocket::quitNotify);
}
VideoSocket::~VideoSocket()
{
quitNotify();
}
qint32 VideoSocket::subThreadRecvData(quint8 *buf, qint32 bufSize)
{
// this function cant call in main thread
Q_ASSERT(QCoreApplication::instance()->thread() != QThread::currentThread());
if (m_quit) {
return 0;
}
QMutexLocker locker(&m_mutex);
m_buffer = buf;
m_bufferSize = bufSize;
m_dataSize = 0;
// post event
VideoSocketEvent *getDataEvent = new VideoSocketEvent();
QCoreApplication::postEvent(this, getDataEvent);
// wait
while (!m_recvData) {
m_recvDataCond.wait(&m_mutex);
}
m_recvData = false;
return m_dataSize;
}
bool VideoSocket::event(QEvent *event)
{
if (static_cast<QScrcpyEvent::Type>(event->type()) == QScrcpyEvent::VideoSocket) {
onReadyRead();
return true;
}
return QTcpSocket::event(event);
}
void VideoSocket::onReadyRead()
{
QMutexLocker locker(&m_mutex);
if (m_buffer && m_bufferSize <= bytesAvailable()) {
// recv data
qint64 readSize = qMin(bytesAvailable(), (qint64)m_bufferSize);
m_dataSize = read((char *)m_buffer, readSize);
m_buffer = Q_NULLPTR;
m_bufferSize = 0;
m_recvData = true;
m_recvDataCond.wakeOne();
}
}
void VideoSocket::quitNotify()
{
m_quit = true;
QMutexLocker locker(&m_mutex);
if (m_buffer) {
m_buffer = Q_NULLPTR;
m_bufferSize = 0;
m_recvData = true;
m_dataSize = 0;
m_recvDataCond.wakeOne();
}
}

View file

@ -0,0 +1,35 @@
#ifndef VIDEOSOCKET_H
#define VIDEOSOCKET_H
#include <QEvent>
#include <QMutex>
#include <QTcpSocket>
#include <QWaitCondition>
class VideoSocket : public QTcpSocket
{
Q_OBJECT
public:
explicit VideoSocket(QObject *parent = nullptr);
virtual ~VideoSocket();
qint32 subThreadRecvData(quint8 *buf, qint32 bufSize);
protected:
bool event(QEvent *event);
protected slots:
void onReadyRead();
void quitNotify();
private:
QMutex m_mutex;
QWaitCondition m_recvDataCond;
bool m_recvData = false;
quint8 *m_buffer = Q_NULLPTR;
qint32 m_bufferSize = 0;
qint32 m_dataSize = 0;
bool m_quit = false;
};
#endif // VIDEOSOCKET_H

View file

@ -0,0 +1,361 @@
#include <QDebug>
#include <QTime>
#include "compat.h"
#include "decoder.h"
#include "recorder.h"
#include "stream.h"
#include "videosocket.h"
#define BUFSIZE 0x10000
#define HEADER_SIZE 12
#define NO_PTS UINT64_MAX
typedef qint32 (*ReadPacketFunc)(void *, quint8 *, qint32);
Stream::Stream(QObject *parent) : QThread(parent) {}
Stream::~Stream() {}
static void avLogCallback(void *avcl, int level, const char *fmt, va_list vl)
{
Q_UNUSED(avcl)
Q_UNUSED(vl)
QString localFmt = QString::fromUtf8(fmt);
localFmt.prepend("[FFmpeg] ");
switch (level) {
case AV_LOG_PANIC:
case AV_LOG_FATAL:
qFatal("%s", localFmt.toUtf8().data());
break;
case AV_LOG_ERROR:
qCritical() << localFmt.toUtf8();
break;
case AV_LOG_WARNING:
qWarning() << localFmt.toUtf8();
break;
case AV_LOG_INFO:
qInfo() << localFmt.toUtf8();
break;
case AV_LOG_DEBUG:
// qDebug() << localFmt.toUtf8();
break;
}
// do not forward others, which are too verbose
return;
}
bool Stream::init()
{
#ifdef QTSCRCPY_LAVF_REQUIRES_REGISTER_ALL
av_register_all();
#endif
if (avformat_network_init()) {
return false;
}
av_log_set_callback(avLogCallback);
return true;
}
void Stream::deInit()
{
avformat_network_deinit(); // ignore failure
}
void Stream::setDecoder(Decoder *decoder)
{
m_decoder = decoder;
}
static quint32 bufferRead32be(quint8 *buf)
{
return static_cast<quint32>((buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]);
}
static quint64 bufferRead64be(quint8 *buf)
{
quint32 msb = bufferRead32be(buf);
quint32 lsb = bufferRead32be(&buf[4]);
return (static_cast<quint64>(msb) << 32) | lsb;
}
void Stream::setVideoSocket(VideoSocket *videoSocket)
{
m_videoSocket = videoSocket;
}
void Stream::setRecoder(Recorder *recorder)
{
m_recorder = recorder;
}
qint32 Stream::recvData(quint8 *buf, qint32 bufSize)
{
if (!buf) {
return 0;
}
if (m_videoSocket) {
qint32 len = m_videoSocket->subThreadRecvData(buf, bufSize);
return len;
}
return 0;
}
bool Stream::startDecode()
{
if (!m_videoSocket) {
return false;
}
start();
return true;
}
void Stream::stopDecode()
{
if (m_decoder) {
m_decoder->interrupt();
}
wait();
}
void Stream::run()
{
AVCodec *codec = Q_NULLPTR;
m_codecCtx = Q_NULLPTR;
m_parser = Q_NULLPTR;
// codec
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
qCritical("H.264 decoder not found");
goto runQuit;
}
// codeCtx
m_codecCtx = avcodec_alloc_context3(codec);
if (!m_codecCtx) {
qCritical("Could not allocate codec context");
goto runQuit;
}
if (m_decoder && !m_decoder->open(codec)) {
qCritical("Could not open m_decoder");
goto runQuit;
}
if (m_recorder) {
if (!m_recorder->open(codec)) {
qCritical("Could not open recorder");
goto runQuit;
}
if (!m_recorder->startRecorder()) {
qCritical("Could not start recorder");
goto runQuit;
}
}
m_parser = av_parser_init(AV_CODEC_ID_H264);
if (!m_parser) {
qCritical("Could not initialize parser");
goto runQuit;
}
// We must only pass complete frames to av_parser_parse2()!
// It's more complicated, but this allows to reduce the latency by 1 frame!
m_parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
for (;;) {
AVPacket packet;
bool ok = recvPacket(&packet);
if (!ok) {
// end of stream
break;
}
ok = pushPacket(&packet);
av_packet_unref(&packet);
if (!ok) {
// cannot process packet (error already logged)
break;
}
}
qDebug("End of frames");
if (m_hasPending) {
av_packet_unref(&m_pending);
}
av_parser_close(m_parser);
runQuit:
if (m_recorder) {
if (m_recorder->isRunning()) {
m_recorder->stopRecorder();
m_recorder->wait();
}
m_recorder->close();
}
if (m_decoder) {
m_decoder->close();
}
if (m_codecCtx) {
avcodec_free_context(&m_codecCtx);
}
emit onStreamStop();
}
bool Stream::recvPacket(AVPacket *packet)
{
// The video stream contains raw packets, without time information. When we
// record, we retrieve the timestamps separately, from a "meta" header
// added by the server before each raw packet.
//
// The "meta" header length is 12 bytes:
// [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ...
// <-------------> <-----> <-----------------------------...
// PTS packet raw packet
// size
//
// It is followed by <packet_size> bytes containing the packet/frame.
quint8 header[HEADER_SIZE];
qint32 r = recvData(header, HEADER_SIZE);
if (r < HEADER_SIZE) {
return false;
}
quint64 pts = bufferRead64be(header);
quint32 len = bufferRead32be(&header[8]);
Q_ASSERT(pts == NO_PTS || (pts & 0x8000000000000000) == 0);
Q_ASSERT(len);
if (av_new_packet(packet, static_cast<int>(len))) {
qCritical("Could not allocate packet");
return false;
}
r = recvData(packet->data, static_cast<qint32>(len));
if (r < 0 || static_cast<quint32>(r) < len) {
av_packet_unref(packet);
return false;
}
packet->pts = pts != NO_PTS ? static_cast<int64_t>(pts) : static_cast<int64_t>(AV_NOPTS_VALUE);
return true;
}
bool Stream::pushPacket(AVPacket *packet)
{
bool isConfig = packet->pts == AV_NOPTS_VALUE;
// A config packet must not be decoded immetiately (it contains no
// frame); instead, it must be concatenated with the future data packet.
if (m_hasPending || isConfig) {
qint32 offset;
if (m_hasPending) {
offset = m_pending.size;
if (av_grow_packet(&m_pending, packet->size)) {
qCritical("Could not grow packet");
return false;
}
} else {
offset = 0;
if (av_new_packet(&m_pending, packet->size)) {
qCritical("Could not create packet");
return false;
}
m_hasPending = true;
}
memcpy(m_pending.data + offset, packet->data, static_cast<unsigned int>(packet->size));
if (!isConfig) {
// prepare the concat packet to send to the decoder
m_pending.pts = packet->pts;
m_pending.dts = packet->dts;
m_pending.flags = packet->flags;
packet = &m_pending;
}
}
if (isConfig) {
// config packet
bool ok = processConfigPacket(packet);
if (!ok) {
return false;
}
} else {
// data packet
bool ok = parse(packet);
if (m_hasPending) {
// the pending packet must be discarded (consumed or error)
m_hasPending = false;
av_packet_unref(&m_pending);
}
if (!ok) {
return false;
}
}
return true;
}
bool Stream::processConfigPacket(AVPacket *packet)
{
if (m_recorder && !m_recorder->push(packet)) {
qCritical("Could not send config packet to recorder");
return false;
}
return true;
}
bool Stream::parse(AVPacket *packet)
{
quint8 *inData = packet->data;
int inLen = packet->size;
quint8 *outData = Q_NULLPTR;
int outLen = 0;
int r = av_parser_parse2(m_parser, m_codecCtx, &outData, &outLen, inData, inLen, AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1);
// PARSER_FLAG_COMPLETE_FRAMES is set
Q_ASSERT(r == inLen);
(void)r;
Q_ASSERT(outLen == inLen);
if (m_parser->key_frame == 1) {
packet->flags |= AV_PKT_FLAG_KEY;
}
bool ok = processFrame(packet);
if (!ok) {
qCritical("Could not process frame");
return false;
}
return true;
}
bool Stream::processFrame(AVPacket *packet)
{
if (m_decoder && !m_decoder->push(packet)) {
return false;
}
if (m_recorder) {
packet->dts = packet->pts;
if (!m_recorder->push(packet)) {
qCritical("Could not send packet to recorder");
return false;
}
}
return true;
}

View file

@ -0,0 +1,59 @@
#ifndef STREAM_H
#define STREAM_H
#include <QPointer>
#include <QThread>
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
}
class VideoSocket;
class Recorder;
class Decoder;
class Stream : public QThread
{
Q_OBJECT
public:
Stream(QObject *parent = Q_NULLPTR);
virtual ~Stream();
public:
static bool init();
static void deInit();
void setDecoder(Decoder *decoder);
void setRecoder(Recorder *recorder);
void setVideoSocket(VideoSocket *deviceSocket);
qint32 recvData(quint8 *buf, qint32 bufSize);
bool startDecode();
void stopDecode();
signals:
void onStreamStop();
protected:
void run();
bool recvPacket(AVPacket *packet);
bool pushPacket(AVPacket *packet);
bool processConfigPacket(AVPacket *packet);
bool parse(AVPacket *packet);
bool processFrame(AVPacket *packet);
private:
QPointer<VideoSocket> m_videoSocket;
// for recorder
Recorder *m_recorder = Q_NULLPTR;
Decoder *m_decoder = Q_NULLPTR;
AVCodecContext *m_codecCtx = Q_NULLPTR;
AVCodecParserContext *m_parser = Q_NULLPTR;
// successive packets may need to be concatenated, until a non-config
// packet is available
bool m_hasPending = false;
AVPacket m_pending;
};
#endif // STREAM_H

View file

@ -0,0 +1,6 @@
HEADERS += \
$$PWD/stream.h
SOURCES += \
$$PWD/stream.cpp

View file

@ -3,11 +3,10 @@
#include <QMouseEvent>
#include <QShowEvent>
#include "device.h"
#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)
{
@ -15,8 +14,6 @@ ToolForm::ToolForm(QWidget *adsorbWidget, AdsorbPositions adsorbPos) : MagneticW
setWindowFlags(windowFlags() | Qt::FramelessWindowHint);
//setWindowFlags(windowFlags() & ~Qt::WindowMinMaxButtonsHint);
updateGroupControl();
initStyle();
}
@ -25,14 +22,13 @@ ToolForm::~ToolForm()
delete ui;
}
void ToolForm::setSerial(const QString &serial)
void ToolForm::setDevice(Device *device)
{
m_serial = serial;
}
bool ToolForm::isHost()
{
return m_isHost;
if (!device) {
return;
}
m_device = device;
connect(m_device, &Device::controlStateChange, this, &ToolForm::onControlStateChange);
}
void ToolForm::initStyle()
@ -45,7 +41,6 @@ void ToolForm::initStyle()
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);
@ -56,23 +51,26 @@ void ToolForm::initStyle()
void ToolForm::updateGroupControl()
{
if (m_isHost) {
ui->groupControlBtn->setStyleSheet("color: red");
} else {
ui->groupControlBtn->setStyleSheet("color: green");
if (!m_device) {
return;
}
switch (m_device->controlState()) {
case Device::GroupControlState::GCS_FREE:
ui->groupControlBtn->setStyleSheet("color: #DCDCDC");
break;
case Device::GroupControlState::GCS_HOST:
ui->groupControlBtn->setStyleSheet("color: red");
break;
case Device::GroupControlState::GCS_CLIENT:
ui->groupControlBtn->setStyleSheet("color: green");
break;
}
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();
}
}
@ -85,11 +83,7 @@ void ToolForm::mouseReleaseEvent(QMouseEvent *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();
}
}
@ -108,126 +102,121 @@ void ToolForm::hideEvent(QHideEvent *event)
void ToolForm::on_fullScreenBtn_clicked()
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
dynamic_cast<VideoForm*>(parent())->switchFullScreen();
emit m_device->switchFullScreen();
}
void ToolForm::on_returnBtn_clicked()
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
device->postGoBack();
emit m_device->postGoBack();
}
void ToolForm::on_homeBtn_clicked()
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
device->postGoHome();
emit m_device->postGoHome();
}
void ToolForm::on_menuBtn_clicked()
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
device->postGoMenu();
emit m_device->postGoMenu();
}
void ToolForm::on_appSwitchBtn_clicked()
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
device->postAppSwitch();
emit m_device->postAppSwitch();
}
void ToolForm::on_powerBtn_clicked()
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
device->postPower();
emit m_device->postPower();
}
void ToolForm::on_screenShotBtn_clicked()
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
device->screenshot();
emit m_device->screenshot();
}
void ToolForm::on_volumeUpBtn_clicked()
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
device->postVolumeUp();
emit m_device->postVolumeUp();
}
void ToolForm::on_volumeDownBtn_clicked()
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
device->postVolumeDown();
emit m_device->postVolumeDown();
}
void ToolForm::on_closeScreenBtn_clicked()
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
device->setDisplayPower(false);
emit m_device->setScreenPowerMode(ControlMsg::SPM_OFF);
}
void ToolForm::on_expandNotifyBtn_clicked()
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
device->expandNotificationPanel();
emit m_device->expandNotificationPanel();
}
void ToolForm::on_touchBtn_clicked()
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
m_showTouch = !m_showTouch;
device->showTouch(m_showTouch);
emit m_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) {
if (!m_device) {
return;
}
device->setDisplayPower(true);
Device::GroupControlState state = m_device->controlState();
if (state == Device::GroupControlState::GCS_FREE) {
emit m_device->setControlState(m_device, Device::GroupControlState::GCS_HOST);
}
if (state == Device::GroupControlState::GCS_HOST) {
emit m_device->setControlState(m_device, Device::GroupControlState::GCS_FREE);
}
}
void ToolForm::onControlStateChange(Device *device, Device::GroupControlState oldState, Device::GroupControlState newState)
{
Q_UNUSED(device)
Q_UNUSED(oldState)
Q_UNUSED(newState)
updateGroupControl();
}

View file

@ -4,7 +4,7 @@
#include <QPointer>
#include <QWidget>
#include "../QtScrcpyCore/include/QtScrcpyCore.h"
#include "device.h"
#include "magneticwidget.h"
namespace Ui
@ -21,8 +21,7 @@ public:
explicit ToolForm(QWidget *adsorbWidget, AdsorbPositions adsorbPos);
~ToolForm();
void setSerial(const QString& serial);
bool isHost();
void setDevice(Device *device);
protected:
void mousePressEvent(QMouseEvent *event);
@ -46,7 +45,8 @@ private slots:
void on_expandNotifyBtn_clicked();
void on_touchBtn_clicked();
void on_groupControlBtn_clicked();
void on_openScreenBtn_clicked();
void onControlStateChange(Device *device, Device::GroupControlState oldState, Device::GroupControlState newState);
private:
void initStyle();
@ -55,9 +55,8 @@ private:
private:
Ui::ToolForm *ui;
QPoint m_dragPosition;
QString m_serial;
QPointer<Device> m_device;
bool m_showTouch = false;
bool m_isHost = false;
};
#endif // TOOLFORM_H

View file

@ -22,9 +22,6 @@
</property>
<item>
<widget class="QPushButton" name="groupControlBtn">
<property name="toolTip">
<string>group control</string>
</property>
<property name="text">
<string/>
</property>
@ -73,16 +70,6 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="openScreenBtn">
<property name="toolTip">
<string>open screen</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="closeScreenBtn">
<property name="toolTip">

11
QtScrcpy/device/ui/ui.pri Normal file
View file

@ -0,0 +1,11 @@
SOURCES += \
$$PWD/videoform.cpp \
$$PWD/toolform.cpp
HEADERS += \
$$PWD/videoform.h \
$$PWD/toolform.h
FORMS += \
$$PWD/videoform.ui \
$$PWD/toolform.ui

View file

@ -1,4 +1,4 @@
// #include <QDesktopWidget>
#include <QDesktopWidget>
#include <QFileInfo>
#include <QLabel>
#include <QMessageBox>
@ -13,26 +13,26 @@
#include <QWindow>
#include <QtWidgets/QHBoxLayout>
#if defined(Q_OS_WIN32)
#include <Windows.h>
#endif
#include "config.h"
#include "controller.h"
#include "device.h"
#include "iconhelper.h"
#include "qyuvopenglwidget.h"
#include "toolform.h"
#include "mousetap/mousetap.h"
#include "ui_videoform.h"
#include "videoform.h"
extern "C"
{
#include "libavutil/frame.h"
}
VideoForm::VideoForm(bool framelessWindow, bool skin, bool showToolbar, QWidget *parent) : QWidget(parent), ui(new Ui::videoForm), m_skin(skin)
VideoForm::VideoForm(bool framelessWindow, bool skin, 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);
}
@ -48,6 +48,7 @@ VideoForm::~VideoForm()
void VideoForm::initUI()
{
setAttribute(Qt::WA_DeleteOnClose);
if (m_skin) {
QPixmap phone;
if (phone.load(":/res/phone.png")) {
@ -65,8 +66,8 @@ void VideoForm::initUI()
m_videoWidget = new QYUVOpenGLWidget();
m_videoWidget->hide();
ui->keepRatioWidget->setWidget(m_videoWidget);
ui->keepRatioWidget->setWidthHeightRatio(m_widthHeightRatio);
ui->keepRadioWidget->setWidget(m_videoWidget);
ui->keepRadioWidget->setWidthHeightRadio(m_widthHeightRatio);
m_fpsLabel = new QLabel(m_videoWidget);
QFont ft;
@ -80,41 +81,31 @@ void VideoForm::initUI()
setMouseTracking(true);
m_videoWidget->setMouseTracking(true);
ui->keepRatioWidget->setMouseTracking(true);
ui->keepRadioWidget->setMouseTracking(true);
}
QRect VideoForm::getGrabCursorRect()
{
QRect rc;
#if defined(Q_OS_WIN32)
rc = QRect(ui->keepRatioWidget->mapToGlobal(m_videoWidget->pos()), m_videoWidget->size());
rc = QRect(ui->keepRadioWidget->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.setTopLeft(rc.topLeft() * m_videoWidget->devicePixelRatio());
rc.setBottomRight(rc.bottomRight() * m_videoWidget->devicePixelRatio());
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.setTopLeft(ui->keepRadioWidget->mapToGlobal(rc.topLeft()));
rc.setBottomRight(ui->keepRadioWidget->mapToGlobal(rc.bottomRight()));
rc.setX(rc.x() + 100);
rc.setY(rc.y() + 30);
rc.setWidth(rc.width() - 180);
rc.setHeight(rc.height() - 60);
#else
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;
}
@ -136,7 +127,7 @@ void VideoForm::resizeSquare()
void VideoForm::removeBlackRect()
{
resize(ui->keepRatioWidget->goodSize());
resize(ui->keepRadioWidget->goodSize());
}
void VideoForm::showFPS(bool show)
@ -147,7 +138,7 @@ void VideoForm::showFPS(bool show)
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)
void VideoForm::updateRender(const AVFrame *frame)
{
if (m_videoWidget->isHidden()) {
if (m_loadingWidget) {
@ -156,21 +147,16 @@ void VideoForm::updateRender(int width, int height, uint8_t* dataY, uint8_t* dat
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;
updateShowSize(QSize(frame->width, frame->height));
m_videoWidget->setFrameSize(QSize(frame->width, frame->height));
m_videoWidget->updateTextures(frame->data[0], frame->data[1], frame->data[2], frame->linesize[0], frame->linesize[1], frame->linesize[2]);
}
void VideoForm::showToolForm(bool show)
{
if (!m_toolForm) {
m_toolForm = new ToolForm(this, ToolForm::AP_OUTSIDE_RIGHT);
m_toolForm->setSerial(m_serial);
m_toolForm->setDevice(m_device);
}
m_toolForm->move(pos().x() + geometry().width(), pos().y() + 30);
m_toolForm->setVisible(show);
@ -193,192 +179,155 @@ void VideoForm::installShortcut()
// 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) {
if (!m_device) {
return;
}
switchFullScreen();
emit m_device->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);
shortcut = new QShortcut(QKeySequence("Ctrl+x"), this);
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) {
if (!m_device) {
return;
}
device->postGoHome();
emit m_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) {
if (!m_device) {
return;
}
device->postGoBack();
emit m_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) {
if (!m_device) {
return;
}
emit device->postAppSwitch();
emit m_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) {
if (!m_device) {
return;
}
device->postGoMenu();
emit m_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) {
if (!m_device) {
return;
}
emit device->postVolumeUp();
emit m_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) {
if (!m_device) {
return;
}
emit device->postVolumeDown();
emit m_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) {
if (!m_device) {
return;
}
emit device->postPower();
emit m_device->postPower();
});
// setScreenPowerMode(ControlMsg::SPM_OFF)
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) {
if (!m_device) {
return;
}
emit device->setDisplayPower(false);
emit m_device->setScreenPowerMode(ControlMsg::SPM_OFF);
});
// 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) {
if (!m_device) {
return;
}
emit device->expandNotificationPanel();
emit m_device->expandNotificationPanel();
});
// collapsePanel
// collapseNotificationPanel
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) {
if (!m_device) {
return;
}
emit device->collapsePanel();
emit m_device->collapseNotificationPanel();
});
// copy
// requestDeviceClipboard
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) {
if (!m_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();
emit m_device->requestDeviceClipboard();
});
// 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) {
if (!m_device) {
return;
}
emit device->setDeviceClipboard();
emit m_device->clipboardPaste();
});
// 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) {
if (!m_device) {
return;
}
emit device->clipboardPaste();
emit m_device->setDeviceClipboard();
});
}
QRect VideoForm::getScreenRect()
{
QRect screenRect;
QScreen *screen = QGuiApplication::primaryScreen();
QWidget *win = window();
if (win) {
QWindow *winHandle = win->windowHandle();
if (winHandle) {
screen = winHandle->screen();
}
if (!win) {
return screenRect;
}
if (screen) {
screenRect = screen->availableGeometry();
QWindow *winHandle = win->windowHandle();
if (!winHandle) {
return screenRect;
}
QScreen *screen = winHandle->screen();
if (!screen) {
return screenRect;
}
screenRect = screen->availableGeometry();
return screenRect;
}
@ -419,7 +368,7 @@ void VideoForm::updateShowSize(const QSize &newSize)
m_frameSize = newSize;
m_widthHeightRatio = 1.0f * newSize.width() / newSize.height();
ui->keepRatioWidget->setWidthHeightRatio(m_widthHeightRatio);
ui->keepRadioWidget->setWidthHeightRadio(m_widthHeightRatio);
bool vertical = m_widthHeightRatio < 1.0f ? true : false;
QSize showSize = newSize;
@ -436,14 +385,9 @@ void VideoForm::updateShowSize(const QSize &newSize)
showSize.setHeight(showSize.width() / m_widthHeightRatio);
}
if (isFullScreen() && qsc::IDeviceManage::getInstance().getDevice(m_serial)) {
switchFullScreen();
if (isFullScreen() && m_device) {
emit m_device->switchFullScreen();
}
if (isMaximized()) {
showNormal();
}
if (m_skin) {
QMargins m = getMargins(vertical);
showSize.setWidth(showSize.width() + m.left() + m.right());
@ -460,17 +404,15 @@ void VideoForm::updateShowSize(const QSize &newSize)
}
}
void VideoForm::switchFullScreen()
void VideoForm::onSwitchFullScreen()
{
if (isFullScreen()) {
// 横屏全屏铺满全屏,恢复时,恢复保持宽高比
if (m_widthHeightRatio > 1.0f) {
ui->keepRatioWidget->setWidthHeightRatio(m_widthHeightRatio);
ui->keepRadioWidget->setWidthHeightRadio(m_widthHeightRatio);
}
showNormal();
// back to normal size.
resize(m_normalSize);
// fullscreen window will move (0,0). qt bug?
move(m_fullScreenBeforePos);
@ -481,19 +423,16 @@ void VideoForm::switchFullScreen()
if (m_skin) {
updateStyleSheet(m_frameSize.height() > m_frameSize.width());
}
showToolForm(this->show_toolbar);
showToolForm(true);
#ifdef Q_OS_WIN32
::SetThreadExecutionState(ES_CONTINUOUS);
#endif
} else {
// 横屏全屏铺满全屏,不保持宽高比
if (m_widthHeightRatio > 1.0f) {
ui->keepRatioWidget->setWidthHeightRatio(-1.0f);
ui->keepRadioWidget->setWidthHeightRadio(-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
@ -513,14 +452,6 @@ void VideoForm::switchFullScreen()
}
}
bool VideoForm::isHost()
{
if (!m_toolForm) {
return false;
}
return m_toolForm->isHost();
}
void VideoForm::updateFPS(quint32 fps)
{
//qDebug() << "FPS:" << fps;
@ -530,17 +461,6 @@ void VideoForm::updateFPS(quint32 fps)
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;
@ -556,49 +476,36 @@ void VideoForm::staysOnTop(bool top)
}
}
void VideoForm::setDevice(Device *device)
{
m_device = device;
}
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 (m_device) {
emit m_device->postGoHome();
}
}
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) {
if (!m_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());
event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint()));
emit m_device->mouseEvent(event, 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();
qreal x = event->localPos().x() / m_videoWidget->size().width();
qreal y = event->localPos().y() / m_videoWidget->size().height();
QString posTip = QString(R"("pos": {"x": %1, "y": %2})").arg(x).arg(y);
qInfo() << posTip.toStdString().c_str();
qInfo(posTip.toStdString().c_str());
}
} else {
if (event->button() == Qt::LeftButton) {
m_dragPosition = globalPos.toPoint() - frameGeometry().topLeft();
m_dragPosition = event->globalPos() - frameGeometry().topLeft();
event->accept();
}
}
@ -606,20 +513,13 @@ void VideoForm::mousePressEvent(QMouseEvent *event)
void VideoForm::mouseReleaseEvent(QMouseEvent *event)
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (m_dragPosition.isNull()) {
if (!device) {
if (!m_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
event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint()));
// local check
QPointF local = m_videoWidget->mapFrom(this, localPos.toPoint());
QPointF local = event->localPos();
if (local.x() < 0) {
local.setX(0);
}
@ -632,8 +532,8 @@ void VideoForm::mouseReleaseEvent(QMouseEvent *event)
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());
event->setLocalPos(local);
emit m_device->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size());
} else {
m_dragPosition = QPoint(0, 0);
}
@ -641,24 +541,15 @@ void VideoForm::mouseReleaseEvent(QMouseEvent *event)
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) {
if (!m_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());
event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint()));
emit m_device->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size());
} else if (!m_dragPosition.isNull()) {
if (event->buttons() & Qt::LeftButton) {
move(globalPos.toPoint() - m_dragPosition);
move(event->globalPos() - m_dragPosition);
event->accept();
}
}
@ -666,91 +557,65 @@ void VideoForm::mouseMoveEvent(QMouseEvent *event)
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();
}
removeBlackRect();
}
if (event->button() == Qt::RightButton && device && !device->isCurrentCustomKeymap()) {
emit device->postBackOrScreenOn(event->type() == QEvent::MouseButtonPress);
if (event->button() == Qt::RightButton && m_device) {
emit m_device->postBackOrScreenOn();
}
if (m_videoWidget->geometry().contains(event->pos())) {
if (!device) {
if (!m_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());
event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint()));
emit m_device->mouseEvent(event, 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) {
if (!m_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());
/*
QWheelEvent(const QPointF &pos, const QPointF& globalPos, int delta,
Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers,
Qt::Orientation orient = Qt::Vertical);
*/
QWheelEvent wheelEvent(pos, event->globalPosF(), event->delta(), event->buttons(), event->modifiers(), event->orientation());
emit m_device->wheelEvent(&wheelEvent, m_videoWidget->frameSize(), m_videoWidget->size());
}
}
void VideoForm::keyPressEvent(QKeyEvent *event)
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
if (Qt::Key_Escape == event->key() && !event->isAutoRepeat() && isFullScreen()) {
switchFullScreen();
emit m_device->switchFullScreen();
}
emit device->keyEvent(event, m_videoWidget->frameSize(), m_videoWidget->size());
emit m_device->keyEvent(event, m_videoWidget->frameSize(), m_videoWidget->size());
}
void VideoForm::keyReleaseEvent(QKeyEvent *event)
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
emit device->keyEvent(event, m_videoWidget->frameSize(), m_videoWidget->size());
emit m_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);
}
@ -758,22 +623,20 @@ void VideoForm::paintEvent(QPaintEvent *paint)
void VideoForm::showEvent(QShowEvent *event)
{
Q_UNUSED(event)
if (!isFullScreen() && this->show_toolbar) {
QTimer::singleShot(500, this, [this](){
showToolForm(this->show_toolbar);
});
if (!isFullScreen()) {
showToolForm();
}
}
void VideoForm::resizeEvent(QResizeEvent *event)
{
Q_UNUSED(event)
QSize goodSize = ui->keepRatioWidget->goodSize();
QSize goodSize = ui->keepRadioWidget->goodSize();
if (goodSize.isEmpty()) {
return;
}
QSize curSize = size();
// 限制VideoForm尺寸不能小于keepRatioWidget good size
// 限制VideoForm尺寸不能小于keepRadioWidget good size
if (m_widthHeightRatio > 1.0f) {
// hor
if (curSize.height() <= goodSize.height()) {
@ -794,12 +657,10 @@ void VideoForm::resizeEvent(QResizeEvent *event)
void VideoForm::closeEvent(QCloseEvent *event)
{
Q_UNUSED(event)
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
Config::getInstance().setRect(device->getSerial(), geometry());
device->disconnectDevice();
Config::getInstance().setRect(m_device->getSerial(), geometry());
}
void VideoForm::dragEnterEvent(QDragEnterEvent *event)
@ -819,26 +680,21 @@ void VideoForm::dragLeaveEvent(QDragLeaveEvent *event)
void VideoForm::dropEvent(QDropEvent *event)
{
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (!device) {
if (!m_device) {
return;
}
const QMimeData *qm = event->mimeData();
QList<QUrl> urls = qm->urls();
QString file = qm->urls()[0].toLocalFile();
QFileInfo fileInfo(file);
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());
if (!fileInfo.exists()) {
QMessageBox::warning(this, "QtScrcpy", tr("file does not exist"), QMessageBox::Ok);
return;
}
if (fileInfo.isFile() && fileInfo.suffix() == "apk") {
emit m_device->installApkRequest(m_device->getSerial(), file);
return;
}
emit m_device->pushFileRequest(m_device->getSerial(), file, Config::getInstance().getPushFilePath() + fileInfo.fileName());
}

View file

@ -0,0 +1,87 @@
#ifndef VIDEOFORM_H
#define VIDEOFORM_H
#include <QPointer>
#include <QWidget>
namespace Ui
{
class videoForm;
}
struct AVFrame;
class ToolForm;
class Device;
class FileHandler;
class QYUVOpenGLWidget;
class QLabel;
class VideoForm : public QWidget
{
Q_OBJECT
public:
explicit VideoForm(bool framelessWindow = false, bool skin = true, QWidget *parent = 0);
~VideoForm();
void staysOnTop(bool top = true);
void updateShowSize(const QSize &newSize);
void updateRender(const AVFrame *frame);
void setDevice(Device *device);
QRect getGrabCursorRect();
const QSize &frameSize();
void resizeSquare();
void removeBlackRect();
void showFPS(bool show);
public slots:
void onSwitchFullScreen();
void updateFPS(quint32 fps);
private:
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);
void mouseReleaseEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mouseDoubleClickEvent(QMouseEvent *event);
void wheelEvent(QWheelEvent *event);
void keyPressEvent(QKeyEvent *event);
void keyReleaseEvent(QKeyEvent *event);
void paintEvent(QPaintEvent *);
void showEvent(QShowEvent *event);
void resizeEvent(QResizeEvent *event);
void closeEvent(QCloseEvent *event);
void dragEnterEvent(QDragEnterEvent *event);
void dragMoveEvent(QDragMoveEvent *event);
void dragLeaveEvent(QDragLeaveEvent *event);
void dropEvent(QDropEvent *event);
private:
// ui
Ui::videoForm *ui;
QPointer<ToolForm> m_toolForm;
QPointer<QWidget> m_loadingWidget;
QPointer<QYUVOpenGLWidget> m_videoWidget;
QPointer<QLabel> m_fpsLabel;
//inside member
QSize m_frameSize;
QPoint m_dragPosition;
float m_widthHeightRatio = 0.5f;
bool m_skin = true;
QPoint m_fullScreenBeforePos;
//outside member
QPointer<Device> m_device;
};
#endif // VIDEOFORM_H

View file

@ -39,15 +39,15 @@
<number>0</number>
</property>
<item>
<widget class="KeepRatioWidget" name="keepRatioWidget" native="true"/>
<widget class="KeepRadioWidget" name="keepRadioWidget" native="true"/>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>KeepRatioWidget</class>
<class>KeepRadioWidget</class>
<extends>QWidget</extends>
<header location="global">keepratiowidget.h</header>
<header location="global">keepradiowidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>

View file

@ -0,0 +1,299 @@
#include <QDebug>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QWheelEvent>
#include "devicemanage.h"
#include "server.h"
#include "videoform.h"
#define DM_MAX_DEVICES_NUM 16
DeviceManage::DeviceManage(QObject *parent) : QObject(parent) {}
DeviceManage::~DeviceManage() {}
bool DeviceManage::connectDevice(Device::DeviceParams params)
{
if (params.serial.trimmed().isEmpty()) {
return false;
}
if (m_devices.contains(params.serial)) {
return false;
}
if (DM_MAX_DEVICES_NUM < m_devices.size()) {
qInfo("over the maximum number of connections");
return false;
}
/*
// 没有必要分配端口都用27183即可连接建立以后server会释放监听的
quint16 port = 0;
if (params.useReverse) {
port = getFreePort();
if (0 == port) {
qInfo("no port available, automatically switch to forward");
params.useReverse = false;
} else {
params.localPort = port;
qInfo("free port %d", port);
}
}
*/
Device *device = new Device(params);
connect(device, &Device::deviceDisconnect, this, &DeviceManage::onDeviceDisconnect);
connect(device, &Device::controlStateChange, this, &DeviceManage::onControlStateChange);
m_devices[params.serial] = device;
if (!m_script.isEmpty()) {
device->updateScript(m_script);
}
return true;
}
void DeviceManage::updateScript(QString script)
{
m_script = script;
QMapIterator<QString, QPointer<Device>> i(m_devices);
while (i.hasNext()) {
i.next();
if (i.value()) {
i.value()->updateScript(script);
}
}
}
bool DeviceManage::staysOnTop(const QString &serial)
{
if (!serial.isEmpty() && m_devices.contains(serial)) {
auto it = m_devices.find(serial);
if (!it->data()) {
return false;
}
if (!it->data()->getVideoForm()) {
return false;
}
it->data()->getVideoForm()->staysOnTop();
}
return true;
}
void DeviceManage::showFPS(const QString &serial, bool show)
{
if (!serial.isEmpty() && m_devices.contains(serial)) {
auto it = m_devices.find(serial);
if (!it->data()) {
return;
}
if (!it->data()->getVideoForm()) {
return;
}
it->data()->getVideoForm()->showFPS(show);
}
return;
}
bool DeviceManage::disconnectDevice(const QString &serial)
{
bool ret = false;
if (!serial.isEmpty() && m_devices.contains(serial)) {
auto it = m_devices.find(serial);
if (it->data()) {
delete it->data();
ret = true;
}
}
return ret;
}
void DeviceManage::disconnectAllDevice()
{
QMapIterator<QString, QPointer<Device>> i(m_devices);
while (i.hasNext()) {
i.next();
if (i.value()) {
delete i.value();
}
}
}
void DeviceManage::setGroupControlSignals(Device *host, Device *client, bool install)
{
if (!host || !client) {
return;
}
if (install) {
connect(host, &Device::postGoBack, client, &Device::postGoBack);
connect(host, &Device::postGoHome, client, &Device::postGoHome);
connect(host, &Device::postGoMenu, client, &Device::postGoMenu);
connect(host, &Device::postAppSwitch, client, &Device::postAppSwitch);
connect(host, &Device::postPower, client, &Device::postPower);
connect(host, &Device::postVolumeUp, client, &Device::postVolumeUp);
connect(host, &Device::postVolumeDown, client, &Device::postVolumeDown);
connect(host, &Device::setScreenPowerMode, client, &Device::setScreenPowerMode);
connect(host, &Device::expandNotificationPanel, client, &Device::expandNotificationPanel);
connect(host, &Device::collapseNotificationPanel, client, &Device::collapseNotificationPanel);
connect(host, &Device::postBackOrScreenOn, client, &Device::postBackOrScreenOn);
connect(host, &Device::postTextInput, client, &Device::postTextInput);
connect(host, &Device::setDeviceClipboard, client, &Device::setDeviceClipboard);
connect(host, &Device::clipboardPaste, client, &Device::clipboardPaste);
connect(host, &Device::pushFileRequest, client, &Device::pushFileRequest);
connect(host, &Device::installApkRequest, client, &Device::installApkRequest);
connect(host, &Device::screenshot, client, &Device::screenshot);
connect(host, &Device::showTouch, client, &Device::showTouch);
// dont connect requestDeviceClipboard
//connect(host, &Device::requestDeviceClipboard, client, &Device::requestDeviceClipboard);
} else {
disconnect(host, &Device::postGoBack, client, &Device::postGoBack);
disconnect(host, &Device::postGoHome, client, &Device::postGoHome);
disconnect(host, &Device::postGoMenu, client, &Device::postGoMenu);
disconnect(host, &Device::postAppSwitch, client, &Device::postAppSwitch);
disconnect(host, &Device::postPower, client, &Device::postPower);
disconnect(host, &Device::postVolumeUp, client, &Device::postVolumeUp);
disconnect(host, &Device::postVolumeDown, client, &Device::postVolumeDown);
disconnect(host, &Device::setScreenPowerMode, client, &Device::setScreenPowerMode);
disconnect(host, &Device::expandNotificationPanel, client, &Device::expandNotificationPanel);
disconnect(host, &Device::collapseNotificationPanel, client, &Device::collapseNotificationPanel);
disconnect(host, &Device::postBackOrScreenOn, client, &Device::postBackOrScreenOn);
disconnect(host, &Device::postTextInput, client, &Device::postTextInput);
disconnect(host, &Device::setDeviceClipboard, client, &Device::setDeviceClipboard);
disconnect(host, &Device::clipboardPaste, client, &Device::clipboardPaste);
disconnect(host, &Device::pushFileRequest, client, &Device::pushFileRequest);
disconnect(host, &Device::installApkRequest, client, &Device::installApkRequest);
disconnect(host, &Device::screenshot, client, &Device::screenshot);
disconnect(host, &Device::showTouch, client, &Device::showTouch);
}
}
void DeviceManage::setGroupControlHost(Device *host, bool install)
{
QMapIterator<QString, QPointer<Device>> i(m_devices);
while (i.hasNext()) {
i.next();
if (!i.value()) {
continue;
}
if (i.value() == host) {
continue;
}
if (install) {
if (host) {
setGroupControlSignals(host, i.value(), true);
}
emit i.value()->setControlState(i.value(), Device::GroupControlState::GCS_CLIENT);
} else {
if (host) {
setGroupControlSignals(host, i.value(), false);
}
emit i.value()->setControlState(i.value(), Device::GroupControlState::GCS_FREE);
}
}
}
void DeviceManage::onDeviceDisconnect(QString serial)
{
if (!serial.isEmpty() && m_devices.contains(serial)) {
if (m_devices[serial]->controlState() == Device::GroupControlState::GCS_HOST) {
setGroupControlHost(nullptr, false);
}
m_devices.remove(serial);
}
}
void DeviceManage::onControlStateChange(Device *device, Device::GroupControlState oldState, Device::GroupControlState newState)
{
if (!device) {
return;
}
// free to host
if (oldState == Device::GroupControlState::GCS_FREE && newState == Device::GroupControlState::GCS_HOST) {
// install direct control signals
setGroupControlHost(device, true);
// install convert control signals(frameSize need convert)
connect(device, &Device::mouseEvent, this, &DeviceManage::onMouseEvent, Qt::UniqueConnection);
connect(device, &Device::wheelEvent, this, &DeviceManage::onWheelEvent, Qt::UniqueConnection);
connect(device, &Device::keyEvent, this, &DeviceManage::onKeyEvent, Qt::UniqueConnection);
return;
}
// host to free
if (oldState == Device::GroupControlState::GCS_HOST && newState == Device::GroupControlState::GCS_FREE) {
// uninstall direct control signals
setGroupControlHost(device, false);
// uninstall convert control signals(frameSize need convert)
disconnect(device, &Device::mouseEvent, this, &DeviceManage::onMouseEvent);
disconnect(device, &Device::wheelEvent, this, &DeviceManage::onWheelEvent);
disconnect(device, &Device::keyEvent, this, &DeviceManage::onKeyEvent);
return;
}
}
void DeviceManage::onMouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize)
{
Q_UNUSED(frameSize)
QMapIterator<QString, QPointer<Device>> i(m_devices);
while (i.hasNext()) {
i.next();
if (!i.value()) {
continue;
}
if (i.value() == sender()) {
continue;
}
// neend convert frameSize to its frameSize
emit i.value()->mouseEvent(from, i.value()->frameSize(), showSize);
}
}
void DeviceManage::onWheelEvent(const QWheelEvent *from, const QSize &frameSize, const QSize &showSize)
{
Q_UNUSED(frameSize)
QMapIterator<QString, QPointer<Device>> i(m_devices);
while (i.hasNext()) {
i.next();
if (!i.value()) {
continue;
}
if (i.value() == sender()) {
continue;
}
// neend convert frameSize to its frameSize
emit i.value()->wheelEvent(from, i.value()->frameSize(), showSize);
}
}
void DeviceManage::onKeyEvent(const QKeyEvent *from, const QSize &frameSize, const QSize &showSize)
{
Q_UNUSED(frameSize)
QMapIterator<QString, QPointer<Device>> i(m_devices);
while (i.hasNext()) {
i.next();
if (!i.value()) {
continue;
}
if (i.value() == sender()) {
continue;
}
// neend convert frameSize to its frameSize
emit i.value()->keyEvent(from, i.value()->frameSize(), showSize);
}
}
quint16 DeviceManage::getFreePort()
{
quint16 port = m_localPortStart;
while (port < m_localPortStart + DM_MAX_DEVICES_NUM) {
bool used = false;
QMapIterator<QString, QPointer<Device>> i(m_devices);
while (i.hasNext()) {
i.next();
auto device = i.value();
if (device && device->getServer() && device->getServer()->isReverse() && port == device->getServer()->getParams().localPort) {
used = true;
break;
}
}
if (!used) {
return port;
}
port++;
}
return 0;
}

View file

@ -0,0 +1,46 @@
#ifndef DEVICEMANAGE_H
#define DEVICEMANAGE_H
#include <QMap>
#include <QPointer>
#include "device.h"
class DeviceManage : public QObject
{
Q_OBJECT
public:
explicit DeviceManage(QObject *parent = nullptr);
virtual ~DeviceManage();
bool connectDevice(Device::DeviceParams params);
void updateScript(QString script);
bool staysOnTop(const QString &serial);
void showFPS(const QString &serial, bool show);
bool disconnectDevice(const QString &serial);
void disconnectAllDevice();
protected:
void setGroupControlSignals(Device *host, Device *client, bool install);
void setGroupControlHost(Device *host, bool install);
protected slots:
void onDeviceDisconnect(QString serial);
void onControlStateChange(Device *device, Device::GroupControlState oldState, Device::GroupControlState newState);
// neend convert frameSize to its frameSize
void onMouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize);
void onWheelEvent(const QWheelEvent *from, const QSize &frameSize, const QSize &showSize);
void onKeyEvent(const QKeyEvent *from, const QSize &frameSize, const QSize &showSize);
private:
quint16 getFreePort();
private:
QMap<QString, QPointer<Device>> m_devices;
quint16 m_localPortStart = 27183;
QString m_script;
};
#endif // DEVICEMANAGE_H

View file

@ -0,0 +1,5 @@
HEADERS += \
$$PWD/devicemanage.h
SOURCES += \
$$PWD/devicemanage.cpp

411
QtScrcpy/dialog.cpp Normal file
View file

@ -0,0 +1,411 @@
#include <QDebug>
#include <QFile>
#include <QFileDialog>
#include <QKeyEvent>
#include <QTime>
#include <QTimer>
#include "config.h"
#include "device.h"
#include "dialog.h"
#include "keymap.h"
#include "ui_dialog.h"
#include "videoform.h"
Dialog::Dialog(QWidget *parent) : QDialog(parent), ui(new Ui::Dialog)
{
ui->setupUi(this);
initUI();
connect(&m_adb, &AdbProcess::adbProcessResult, this, [this](AdbProcess::ADB_EXEC_RESULT processResult) {
QString log = "";
bool newLine = true;
QStringList args = m_adb.arguments();
switch (processResult) {
case AdbProcess::AER_ERROR_START:
break;
case AdbProcess::AER_SUCCESS_START:
log = "adb run";
newLine = false;
break;
case AdbProcess::AER_ERROR_EXEC:
//log = m_adb.getErrorOut();
if (args.contains("ifconfig") && args.contains("wlan0")) {
getIPbyIp();
}
break;
case AdbProcess::AER_ERROR_MISSING_BINARY:
log = "adb not find";
break;
case AdbProcess::AER_SUCCESS_EXEC:
//log = m_adb.getStdOut();
if (args.contains("devices")) {
QStringList devices = m_adb.getDevicesSerialFromStdOut();
ui->serialBox->clear();
for (auto &item : devices) {
ui->serialBox->addItem(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->setText(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->setText(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->setText(ip);
}
break;
}
if (!log.isEmpty()) {
outLog(log, newLine);
}
});
}
Dialog::~Dialog()
{
m_deviceManage.disconnectAllDevice();
delete ui;
}
void Dialog::initUI()
{
setAttribute(Qt::WA_DeleteOnClose);
setWindowFlags(windowFlags() | Qt::WindowMinimizeButtonHint);
ui->bitRateBox->addItem("2000000");
ui->bitRateBox->addItem("6000000");
ui->bitRateBox->addItem("8000000");
ui->bitRateBox->addItem("10000000");
ui->bitRateBox->setCurrentIndex(Config::getInstance().getBitRateIndex());
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->maxSizeBox->setCurrentIndex(Config::getInstance().getMaxSizeIndex());
ui->formatBox->addItem("mp4");
ui->formatBox->addItem("mkv");
ui->formatBox->setCurrentIndex(Config::getInstance().getRecordFormatIndex());
ui->recordPathEdt->setText(Config::getInstance().getRecordPath());
ui->framelessCheck->setChecked(Config::getInstance().getFramelessWindow());
#ifdef Q_OS_OSX
// mac need more width
setFixedWidth(520);
#endif
#ifdef Q_OS_LINUX
// linux need more width
setFixedWidth(480);
#endif
}
void Dialog::execAdbCmd()
{
if (checkAdbRun()) {
return;
}
QString cmd = ui->adbCommandEdt->text().trimmed();
outLog("adb " + cmd, false);
m_adb.execute(ui->serialBox->currentText().trimmed(), cmd.split(" ", QString::SkipEmptyParts));
}
QString Dialog::getGameScript(const QString &fileName)
{
QFile loadFile(KeyMap::getKeyMapPath() + "/" + fileName);
if (!loadFile.open(QIODevice::ReadOnly)) {
outLog("open file failed:" + fileName, true);
return "";
}
QString ret = loadFile.readAll();
loadFile.close();
return ret;
}
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);
QString absFilePath;
if (ui->recordScreenCheck->isChecked()) {
QString fileDir(ui->recordPathEdt->text().trimmed());
if (!fileDir.isEmpty()) {
QDateTime dateTime = QDateTime::currentDateTime();
QString fileName = dateTime.toString("_yyyyMMdd_hhmmss_zzz");
QString ext = ui->formatBox->currentText().trimmed();
fileName = windowTitle() + fileName + "." + ext;
QDir dir(fileDir);
absFilePath = dir.absoluteFilePath(fileName);
}
}
quint32 bitRate = ui->bitRateBox->currentText().trimmed().toUInt();
// this is ok that "native" toUshort is 0
quint16 videoSize = ui->maxSizeBox->currentText().trimmed().toUShort();
Device::DeviceParams params;
params.serial = ui->serialBox->currentText().trimmed();
params.maxSize = videoSize;
params.bitRate = bitRate;
// on devices with Android >= 10, the capture frame rate can be limited
params.maxFps = static_cast<quint32>(Config::getInstance().getMaxFps());
params.recordFileName = absFilePath;
params.closeScreen = ui->closeScreenCheck->isChecked();
params.useReverse = ui->useReverseCheck->isChecked();
params.display = !ui->notDisplayCheck->isChecked();
params.renderExpiredFrames = Config::getInstance().getRenderExpiredFrames();
m_deviceManage.connectDevice(params);
if (ui->alwaysTopCheck->isChecked()) {
m_deviceManage.staysOnTop(params.serial);
}
m_deviceManage.showFPS(params.serial, ui->fpsCheck->isChecked());
}
void Dialog::on_stopServerBtn_clicked()
{
if (m_deviceManage.disconnectDevice(ui->serialBox->currentText().trimmed())) {
outLog("stop server");
}
}
void Dialog::on_wirelessConnectBtn_clicked()
{
if (checkAdbRun()) {
return;
}
QString addr = ui->deviceIpEdt->text().trimmed();
if (!ui->devicePortEdt->text().isEmpty()) {
addr += ":";
addr += ui->devicePortEdt->text().trimmed();
} else if (!ui->devicePortEdt->placeholderText().isEmpty()) {
addr += ":";
addr += ui->devicePortEdt->placeholderText().trimmed();
} else {
outLog("error: device port is null", false);
return;
}
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("<br/>");
}
});
}
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::on_wirelessDisConnectBtn_clicked()
{
if (checkAdbRun()) {
return;
}
QString addr = ui->deviceIpEdt->text().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)
{
Config::getInstance().setRecordPath(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()
{
m_deviceManage.disconnectAllDevice();
}
void Dialog::on_refreshGameScriptBtn_clicked()
{
ui->gameBox->clear();
QDir dir(KeyMap::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()
{
m_deviceManage.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_bitRateBox_activated(int index)
{
Config::getInstance().setBitRateIndex(index);
}
void Dialog::on_maxSizeBox_activated(int index)
{
Config::getInstance().setMaxSizeIndex(index);
}
void Dialog::on_formatBox_activated(int index)
{
Config::getInstance().setRecordFormatIndex(index);
}
void Dialog::on_framelessCheck_stateChanged(int arg1)
{
Q_UNUSED(arg1)
Config::getInstance().setFramelessWindow(ui->framelessCheck->isChecked());
}

81
QtScrcpy/dialog.h Normal file
View file

@ -0,0 +1,81 @@
#ifndef DIALOG_H
#define DIALOG_H
#include <QDialog>
#include <QPointer>
#include "adbprocess.h"
#include "devicemanage.h"
namespace Ui
{
class Dialog;
}
class QYUVOpenGLWidget;
class Dialog : public QDialog
{
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 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_bitRateBox_activated(int index);
void on_maxSizeBox_activated(int index);
void on_formatBox_activated(int index);
void on_framelessCheck_stateChanged(int arg1);
private:
bool checkAdbRun();
void initUI();
void execAdbCmd();
QString getGameScript(const QString &fileName);
private:
Ui::Dialog *ui;
AdbProcess m_adb;
DeviceManage m_deviceManage;
};
#endif // DIALOG_H

653
QtScrcpy/dialog.ui Normal file
View file

@ -0,0 +1,653 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>420</width>
<height>492</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>420</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>420</width>
<height>16777215</height>
</size>
</property>
<property name="windowTitle">
<string notr="true">QtScrcpy</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="configGroupBox">
<property name="title">
<string>Start Config</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>3</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<widget class="QWidget" name="configWidget1" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>bit rate:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="bitRateBox">
<property name="toolTip">
<string/>
</property>
<property name="currentText">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>max size:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="maxSizeBox">
<property name="toolTip">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>record format</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="formatBox"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="configWidget2" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>record save path:</string>
</property>
<property name="buddy">
<cstring>recordPathEdt</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="recordPathEdt">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="selectRecordPathBtn">
<property name="text">
<string>select path</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="configWidget4" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_8">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QComboBox" name="gameBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="refreshGameScriptBtn">
<property name="text">
<string>refresh script</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="applyScriptBtn">
<property name="text">
<string>apply</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="configWidget3" native="true">
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="1" column="1">
<widget class="QCheckBox" name="closeScreenCheck">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>screen-off</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QCheckBox" name="framelessCheck">
<property name="text">
<string>frameless</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="alwaysTopCheck">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>always on top</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="notDisplayCheck">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>background record</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QCheckBox" name="useReverseCheck">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>reverse connection</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="recordScreenCheck">
<property name="text">
<string>record screen</string>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QCheckBox" name="fpsCheck">
<property name="text">
<string>show fps</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="usbGroupBox">
<property name="title">
<string>USB line</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>3</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<widget class="QWidget" name="usbWidget1" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>device serial:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="serialBox"/>
</item>
<item>
<widget class="QPushButton" name="startServerBtn">
<property name="text">
<string>start server</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stopServerBtn">
<property name="text">
<string>stop server</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="usbWidget2" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="stopAllServerBtn">
<property name="text">
<string>stop all server</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="updateDevice">
<property name="text">
<string>refresh devices</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="getIPBtn">
<property name="text">
<string>get device IP</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="startAdbdBtn">
<property name="text">
<string>start adbd</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="wirelessGroupBox">
<property name="title">
<string>Wireless</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>3</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<widget class="QLineEdit" name="deviceIpEdt">
<property name="text">
<string/>
</property>
<property name="maxLength">
<number>128</number>
</property>
<property name="placeholderText">
<string notr="true">192.168.0.1</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="minimumSize">
<size>
<width>5</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>5</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="devicePortEdt">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>60</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="maxLength">
<number>6</number>
</property>
<property name="placeholderText">
<string notr="true">5555</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="wirelessConnectBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>wireless connect</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="wirelessDisConnectBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>wireless disconnect</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="adbGroupBox">
<property name="title">
<string notr="true">adb</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>3</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>adb command:</string>
</property>
<property name="buddy">
<cstring>adbCommandEdt</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="adbCommandEdt">
<property name="text">
<string notr="true">devices</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="adbCommandBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>execute</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stopAdbBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>terminate</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="clearOut">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>clear</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QTextEdit" name="outEdit">
<property name="minimumSize">
<size>
<width>0</width>
<height>140</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
<property name="documentTitle">
<string/>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<layoutdefault spacing="6" margin="11"/>
<tabstops>
<tabstop>deviceIpEdt</tabstop>
<tabstop>devicePortEdt</tabstop>
<tabstop>wirelessConnectBtn</tabstop>
<tabstop>wirelessDisConnectBtn</tabstop>
<tabstop>adbCommandEdt</tabstop>
<tabstop>adbCommandBtn</tabstop>
<tabstop>stopAdbBtn</tabstop>
<tabstop>clearOut</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View file

@ -0,0 +1,5 @@
HEADERS += \
$$PWD/iconhelper.h
SOURCES += \
$$PWD/iconhelper.cpp

View file

@ -1,443 +0,0 @@
#include <QPointer>
#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<VideoForm*>(data)->isHost();
}
QSize GroupController::getFrameSize(const QString &serial)
{
auto data = qsc::IDeviceManage::getInstance().getDevice(serial)->getUserData();
if (!data) {
return QSize();
}
return static_cast<VideoForm*>(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);
}
}

View file

@ -1,56 +0,0 @@
#ifndef GROUPCONTROLLER_H
#define GROUPCONTROLLER_H
#include <QObject>
#include <QVector>
#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<QString> m_devices;
};
#endif // GROUPCONTROLLER_H

View file

@ -1,4 +1,4 @@
#include <QApplication>
#include <QApplication>
#include <QDebug>
#include <QFile>
#include <QSurfaceFormat>
@ -9,43 +9,36 @@
#include "config.h"
#include "dialog.h"
#include "mousetap/mousetap.h"
#include "stream.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");
qputenv("QTSCRCPY_ADB_PATH", "../../../../third_party/adb/win/adb.exe");
qputenv("QTSCRCPY_SERVER_PATH", "../../../../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
qputenv("QTSCRCPY_ADB_PATH", "../../../QtScrcpy/QtScrcpyCore/src/third_party/adb/linux/adb");
qputenv("QTSCRCPY_SERVER_PATH", "../../../QtScrcpy/QtScrcpyCore/src/third_party/scrcpy-server");
qputenv("QTSCRCPY_KEYMAP_PATH", "../../../keymap");
qputenv("QTSCRCPY_ADB_PATH", "../../../third_party/adb/linux/adb");
qputenv("QTSCRCPY_SERVER_PATH", "../../../third_party/scrcpy-server");
qputenv("QTSCRCPY_CONFIG_PATH", "../../../config");
qputenv("QTSCRCPY_KEYMAP_PATH", "../../../keymap");
#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);
@ -55,14 +48,8 @@ int main(int argc, char *argv[])
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);
@ -77,6 +64,7 @@ int main(int argc, char *argv[])
QSurfaceFormat::setDefaultFormat(varFormat);
g_oldMessageHandler = qInstallMessageHandler(myMessageOutput);
Stream::init();
QApplication a(argc, argv);
// windows下通过qmake VERSION变量或者rc设置版本号和应用名称后这里可以直接拿到
@ -96,7 +84,7 @@ int main(int argc, char *argv[])
MouseTap::getInstance()->initMouseEventTap();
#endif
// load style sheet
//加载样式表
QFile file(":/qss/psblack.css");
if (file.open(QFile::ReadOnly)) {
QString qss = QLatin1String(file.readAll());
@ -106,29 +94,25 @@ int main(int argc, char *argv[])
file.close();
}
qsc::AdbProcess::setAdbPath(Config::getInstance().getAdbPath());
g_mainDlg = new Dialog {};
g_mainDlg = new Dialog;
g_mainDlg->setWindowTitle(Config::getInstance().getTitle());
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 <https://github.com/barry-ran/QtScrcpy>").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") + " <https://lrbnfell4p.feishu.cn/drive/folder/KviYfz5uFlpUT8dXgdjccmfUnse>");
qInfo() << QObject::tr("If you need more professional game keymap mirror software, you can try the following software:");
qInfo() << QString(QObject::tr("QuickAssistant") + " <https://lrbnfell4p.feishu.cn/drive/folder/Hqckfxj5el1Wjpd9uezcX71lnBh>");
qInfo() << QObject::tr("You can contact me with telegram <https://t.me/+Ylf_5V_rDCMyODQ1>");
qInfo(
"%s",
QObject::tr("This software is completely open source and free. Strictly used for illegal purposes, or at your own risk. You can download it at the "
"following address:")
.toUtf8()
.data());
qInfo() << QString("QtScrcpy %1 <https://github.com/barry-ran/QtScrcpy>").arg(QCoreApplication::applicationVersion()).toUtf8();
int ret = a.exec();
delete g_mainDlg;
#if defined(Q_OS_WIN32) || defined(Q_OS_OSX)
MouseTap::getInstance()->quitMouseEventTap();
#endif
Stream::deInit();
return ret;
}
@ -137,78 +121,32 @@ 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;
}
//language = QLocale::English;
QString languagePath = ":/i18n/";
switch (language) {
case QLocale::Chinese:
languagePath += "zh_CN.qm";
languagePath += "QtScrcpy_zh.qm";
break;
case QLocale::English:
default:
languagePath += "en_US.qm";
break;
languagePath += "QtScrcpy_en.qm";
}
auto loaded = translator.load(languagePath);
if (!loaded) {
qWarning() << "Failed to load translation file:" << languagePath;
}
translator.load(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)
{
if (g_oldMessageHandler) {
g_oldMessageHandler(type, context, msg);
}
// 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 (QtDebugMsg < type) {
if (g_mainDlg && g_mainDlg->isVisible() && !g_mainDlg->filterLog(msg)) {
g_mainDlg->outLog(msg);
}
}
if (QtFatalMsg == type) {
//abort();
}

View file

@ -5,11 +5,11 @@
<key>CFBundleDevelopmentRegion</key>
<string>zh-Hans</string>
<key>CFBundleExecutable</key>
<string>QtScrcpy</string>
<string>@EXECUTABLE@</string>
<key>CFBundleGetInfoString</key>
<string>Created by rankun</string>
<key>CFBundleIconFile</key>
<string>QtScrcpy</string>
<string>@ICON@</string>
<key>CFBundleIdentifier</key>
<string>rankun.QtScrcpy</string>
<key>CFBundleInfoDictionaryVersion</key>
@ -19,13 +19,13 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${BUNDLE_VERSION}</string>
<string>1.0.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>${BUNDLE_VERSION}</string>
<string>1.0.0</string>
<key>LSMinimumSystemVersion</key>
<string>10.10</string>
<key>NSAppleEventsUsageDescription</key>

View file

@ -1,51 +0,0 @@
# 声明ts文件
set(QC_TS_FILES
${CMAKE_CURRENT_SOURCE_DIR}/zh_CN.ts
${CMAKE_CURRENT_SOURCE_DIR}/en_US.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的bugcmake 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. 更新tslupdate -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

Binary file not shown.

View file

@ -1,321 +1,337 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en_US">
<context>
<name>Device</name>
<message>
<source>wait current file transfer to complete</source>
<translation type="vanished">wait current file transfer to complete</translation>
</message>
<message>
<source>file transfer complete</source>
<translation type="vanished">file transfer complete</translation>
</message>
<message>
<source>file transfer failed</source>
<translation type="vanished">file transfer failed</translation>
</message>
<message>
<location filename="../../device/device.cpp" line="181"/>
<source>install apk</source>
<translation>install apk</translation>
</message>
<message>
<location filename="../../device/device.cpp" line="183"/>
<source>file transfer</source>
<translation>file transfer</translation>
</message>
<message>
<location filename="../../device/device.cpp" line="187"/>
<source>wait current %1 to complete</source>
<translation>wait current %1 to complete</translation>
</message>
<message>
<location filename="../../device/device.cpp" line="190"/>
<source>%1 complete, save in %2</source>
<translation>%1 complete, save in %2</translation>
</message>
<message>
<source>%1 complete
save in %2</source>
<translation type="vanished">%1 complete\n save in %2</translation>
</message>
<message>
<location filename="../../device/device.cpp" line="193"/>
<source>%1 failed</source>
<translation>%1 failed</translation>
</message>
</context>
<context>
<name>Dialog</name>
<message>
<source>show</source>
<translation>show</translation>
<location filename="../../dialog.ui" line="429"/>
<source>Wireless</source>
<translation>Wireless</translation>
</message>
<message>
<source>quit</source>
<translation>quit</translation>
<location filename="../../dialog.ui" line="513"/>
<source>wireless connect</source>
<translation>wireless connect</translation>
</message>
<message>
<source>original</source>
<translation>original</translation>
<location filename="../../dialog.ui" line="529"/>
<source>wireless disconnect</source>
<translation>wireless disconnect</translation>
</message>
<message>
<source>no lock</source>
<translation>no lock</translation>
<location filename="../../dialog.ui" line="32"/>
<source>Start Config</source>
<translation>Start Config</translation>
</message>
<message>
<source>Notice</source>
<translation>Notice</translation>
</message>
<message>
<source>Hidden here!</source>
<translation>Hidden here!</translation>
<location filename="../../dialog.ui" line="127"/>
<source>record save path:</source>
<translation>record save path:</translation>
</message>
<message>
<location filename="../../dialog.ui" line="144"/>
<location filename="../../dialog.cpp" line="325"/>
<source>select path</source>
<translation>select path</translation>
</message>
<message>
<source>Clear History</source>
<translation>Clear History</translation>
<location filename="../../dialog.ui" line="99"/>
<source>record format</source>
<translation>record format:</translation>
</message>
<message>
<location filename="../../dialog.ui" line="282"/>
<source>record screen</source>
<translation>record screen</translation>
</message>
<message>
<location filename="../../dialog.ui" line="227"/>
<source>frameless</source>
<translation>frameless</translation>
</message>
<message>
<location filename="../../dialog.ui" line="289"/>
<source>show fps</source>
<translation>show fps</translation>
</message>
<message>
<location filename="../../dialog.ui" line="386"/>
<source>stop all server</source>
<translation>stop all server</translation>
</message>
<message>
<location filename="../../dialog.ui" line="563"/>
<source>adb command:</source>
<translation>adb command:</translation>
</message>
<message>
<location filename="../../dialog.ui" line="599"/>
<source>terminate</source>
<translation>terminate</translation>
</message>
<message>
<location filename="../../dialog.ui" line="586"/>
<source>execute</source>
<translation>execute</translation>
</message>
<message>
<location filename="../../dialog.ui" line="612"/>
<source>clear</source>
<translation>clear</translation>
</message>
<message>
<location filename="../../dialog.ui" line="272"/>
<source>reverse connection</source>
<translation>reverse connection</translation>
</message>
<message>
<source>auto enable</source>
<translation type="vanished">auto enable</translation>
</message>
<message>
<location filename="../../dialog.ui" line="256"/>
<source>background record</source>
<translation>background record</translation>
</message>
<message>
<location filename="../../dialog.ui" line="220"/>
<source>screen-off</source>
<translation>screen-off</translation>
</message>
<message>
<location filename="../../dialog.ui" line="189"/>
<source>apply</source>
<translation>apply</translation>
</message>
<message>
<location filename="../../dialog.ui" line="85"/>
<source>max size:</source>
<translation>max size:</translation>
</message>
<message>
<location filename="../../dialog.ui" line="240"/>
<source>always on top</source>
<translation>always on top</translation>
</message>
<message>
<location filename="../../dialog.ui" line="182"/>
<source>refresh script</source>
<translation>refresh script</translation>
</message>
<message>
<location filename="../../dialog.ui" line="403"/>
<source>get device IP</source>
<translation>get device IP</translation>
</message>
<message>
<location filename="../../dialog.ui" line="302"/>
<source>USB line</source>
<translation>USB line</translation>
</message>
<message>
<location filename="../../dialog.ui" line="358"/>
<source>stop server</source>
<translation>stop server</translation>
</message>
<message>
<location filename="../../dialog.ui" line="348"/>
<source>start server</source>
<translation>start server</translation>
</message>
<message>
<location filename="../../dialog.ui" line="338"/>
<source>device serial:</source>
<translation>device serial:</translation>
</message>
<message>
<source>Config</source>
<translation type="vanished">Config</translation>
</message>
<message>
<location filename="../../dialog.ui" line="68"/>
<source>bit rate:</source>
<translation>bit rate:</translation>
</message>
<message>
<location filename="../../dialog.ui" line="413"/>
<source>start adbd</source>
<translation>start adbd</translation>
</message>
<message>
<location filename="../../dialog.ui" line="393"/>
<source>refresh devices</source>
<translation>refresh devices</translation>
</message>
<message>
<location filename="../../dialog.cpp" line="101"/>
<source>original</source>
<translation>original</translation>
</message>
</context>
<context>
<name>QObject</name>
<message>
<source>This software is completely open source and free. Use it at your own risk. You can download it at the following address:</source>
<translation>This software is completely open source and free. Use it at your own risk. You can download it at the following address:</translation>
<source>This software is completely open source and free, you can download it at the following address:</source>
<translation type="vanished">This software is completely open source and free, you can download it at the following address:</translation>
</message>
<message>
<source>QuickMirror</source>
<translation>QuickMirror</translation>
<source>This software is completely open source and free.
Strictly used for illegal purposes, or at your own risk.
You can download it at the following address:</source>
<translation type="vanished">This software is completely open source and free.\nStrictly used for illegal purposes, or at your own risk.\nYou can download it at the following address:</translation>
</message>
<message>
<source>If you need more professional batch control mirror software, you can try the following software:</source>
<translation>If you need more professional batch control mirror software, you can try the following software:</translation>
</message>
<message>
<source>If you need more professional game keymap mirror software, you can try the following software:</source>
<translation>If you need more professional game keymap mirror software, you can try the following software:</translation>
</message>
<message>
<source>QuickAssistant</source>
<translation>QuickAssistant</translation>
</message>
<message>
<source>You can contact me with telegram &lt;https://t.me/+Ylf_5V_rDCMyODQ1&gt;</source>
<translation>You can contact me with telegram &lt;https://t.me/+Ylf_5V_rDCMyODQ1&gt;</translation>
<location filename="../../main.cpp" line="103"/>
<source>This software is completely open source and free. Strictly used for illegal purposes, or at your own risk. You can download it at the following address:</source>
<translation>This software is completely open source and free. Strictly used for illegal purposes, or at your own risk. You can download it at the following address:</translation>
</message>
</context>
<context>
<name>ToolForm</name>
<message>
<location filename="../../device/ui/toolform.ui" line="14"/>
<source>Tool</source>
<translation>Tool</translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="33"/>
<source>full screen</source>
<translation>full screen</translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="56"/>
<source>expand notify</source>
<translation>expand notify</translation>
</message>
<message>
<source>turn off</source>
<translation type="vanished">turn off</translation>
</message>
<message>
<source>turn on</source>
<translation type="vanished">turn on</translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="66"/>
<source>touch switch</source>
<translation>touch switch</translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="76"/>
<source>close screen</source>
<translation>close screen</translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="86"/>
<source>power</source>
<translation>power</translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="96"/>
<source>volume up</source>
<translation>volume up</translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="106"/>
<source>volume down</source>
<translation>volume down</translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="116"/>
<source>app switch</source>
<translation>app switch</translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="126"/>
<source>menu</source>
<translation>menu</translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="136"/>
<source>home</source>
<translation>home</translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="146"/>
<source>return</source>
<translation>return</translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="156"/>
<source>screen shot</source>
<translation>screen shot</translation>
</message>
<message>
<source>open screen</source>
<translation>open screen</translation>
</message>
<message>
<source>group control</source>
<translation>group control</translation>
</message>
</context>
<context>
<name>VideoForm</name>
<message>
<source>wait current file transfer to complete</source>
<translation type="vanished">wait current file transfer to complete</translation>
</message>
<message>
<source>file transfer complete</source>
<translation type="vanished">file transfer complete</translation>
</message>
<message>
<source>file transfer failed</source>
<translation type="vanished">file transfer failed</translation>
</message>
<message>
<location filename="../../device/ui/videoform.cpp" line="671"/>
<source>file does not exist</source>
<translation>file does not exist</translation>
</message>
</context>
<context>
<name>Widget</name>
<name>videoForm</name>
<message>
<source>Wireless</source>
<translation>Wireless</translation>
</message>
<message>
<source>wireless connect</source>
<translation>wireless connect</translation>
</message>
<message>
<source>wireless disconnect</source>
<translation>wireless disconnect</translation>
</message>
<message>
<source>Start Config</source>
<translation>Start Config</translation>
</message>
<message>
<source>select path</source>
<translation>select path</translation>
</message>
<message>
<source>record format</source>
<translation>record format:</translation>
</message>
<message>
<source>record screen</source>
<translation>record screen</translation>
</message>
<message>
<source>frameless</source>
<translation>frameless</translation>
</message>
<message>
<source>Use Simple Mode</source>
<translatorcomment>Use Simple Mode</translatorcomment>
<translation>Use Simple Mode</translation>
</message>
<message>
<source>Simple Mode</source>
<translatorcomment>Simple Mode</translatorcomment>
<translation>Simple Mode</translation>
</message>
<message>
<source>WIFI Connect</source>
<translatorcomment>WIFI Connect</translatorcomment>
<translation>WIFI Connect</translation>
</message>
<message>
<source>USB Connect</source>
<translatorcomment>USB Connect</translatorcomment>
<translation>USB Connect</translation>
</message>
<message>
<source>Double click to connect:</source>
<translation>Double click to connect:</translation>
</message>
<message>
<source>lock orientation:</source>
<translation>lock orientation:</translation>
</message>
<message>
<source>show fps</source>
<translation>show fps</translation>
</message>
<message>
<source>stay awake</source>
<translation>stay awake</translation>
</message>
<message>
<source>device name:</source>
<translatorcomment>device name:</translatorcomment>
<translation>device name:</translation>
</message>
<message>
<source>update name</source>
<translatorcomment>update name</translatorcomment>
<translation>update name</translation>
</message>
<message>
<source>stop all server</source>
<translation>stop all server</translation>
</message>
<message>
<source>adb command:</source>
<translation>adb command:</translation>
</message>
<message>
<source>terminate</source>
<translation>terminate</translation>
</message>
<message>
<source>execute</source>
<translation>execute</translation>
</message>
<message>
<source>clear</source>
<translation>clear</translation>
</message>
<message>
<source>reverse connection</source>
<translation>reverse connection</translation>
</message>
<message>
<source>background record</source>
<translation>background record</translation>
</message>
<message>
<source>screen-off</source>
<translation>screen-off</translation>
</message>
<message>
<source>apply</source>
<translation>apply</translation>
</message>
<message>
<source>max size:</source>
<translation>max size:</translation>
</message>
<message>
<source>always on top</source>
<translation>always on top</translation>
</message>
<message>
<source>refresh script</source>
<translation>refresh script</translation>
</message>
<message>
<source>get device IP</source>
<translation>get device IP</translation>
</message>
<message>
<source>USB line</source>
<translation>USB line</translation>
</message>
<message>
<source>stop server</source>
<translation>stop server</translation>
</message>
<message>
<source>start server</source>
<translation>start server</translation>
</message>
<message>
<source>device serial:</source>
<translation>device serial:</translation>
</message>
<message>
<source>bit rate:</source>
<translation>bit rate:</translation>
</message>
<message>
<source>start adbd</source>
<translation>start adbd</translation>
</message>
<message>
<source>refresh devices</source>
<translation>refresh devices</translation>
</message>
<message>
<source>install sndcpy</source>
<translation>install sndcpy</translation>
</message>
<message>
<source>start audio</source>
<translation>start audio</translation>
</message>
<message>
<source>stop audio</source>
<translation>stop audio</translation>
</message>
<message>
<source>auto update</source>
<translation>auto update</translation>
</message>
<message>
<source>show toolbar</source>
<translation>show toolbar</translation>
</message>
<message>
<source>record save path:</source>
<translation>record save path:</translation>
<source>qrc:/qml/pinwheel.qml</source>
<translation type="vanished">qrc:/qml/pinwheel.qml</translation>
</message>
</context>
</TS>

Binary file not shown.

View file

@ -0,0 +1,337 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
<context>
<name>Device</name>
<message>
<source>wait current file transfer to complete</source>
<translation type="vanished"></translation>
</message>
<message>
<source>file transfer complete</source>
<translation type="vanished"></translation>
</message>
<message>
<source>file transfer failed</source>
<translation type="vanished"></translation>
</message>
<message>
<location filename="../../device/device.cpp" line="181"/>
<source>install apk</source>
<translation>apk</translation>
</message>
<message>
<location filename="../../device/device.cpp" line="183"/>
<source>file transfer</source>
<translation></translation>
</message>
<message>
<location filename="../../device/device.cpp" line="187"/>
<source>wait current %1 to complete</source>
<translation>%1</translation>
</message>
<message>
<location filename="../../device/device.cpp" line="190"/>
<source>%1 complete, save in %2</source>
<translation>%1,%2</translation>
</message>
<message>
<source>%1 complete
save in %2</source>
<translation type="vanished">%1\n %2</translation>
</message>
<message>
<location filename="../../device/device.cpp" line="193"/>
<source>%1 failed</source>
<translation>%1 </translation>
</message>
</context>
<context>
<name>Dialog</name>
<message>
<location filename="../../dialog.ui" line="429"/>
<source>Wireless</source>
<translation>线</translation>
</message>
<message>
<location filename="../../dialog.ui" line="513"/>
<source>wireless connect</source>
<translation>线</translation>
</message>
<message>
<location filename="../../dialog.ui" line="529"/>
<source>wireless disconnect</source>
<translation>线</translation>
</message>
<message>
<location filename="../../dialog.ui" line="32"/>
<source>Start Config</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="127"/>
<source>record save path:</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="144"/>
<location filename="../../dialog.cpp" line="325"/>
<source>select path</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="99"/>
<source>record format</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="282"/>
<source>record screen</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="227"/>
<source>frameless</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="289"/>
<source>show fps</source>
<translation>fps</translation>
</message>
<message>
<location filename="../../dialog.ui" line="386"/>
<source>stop all server</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="563"/>
<source>adb command:</source>
<translation>adb命令</translation>
</message>
<message>
<location filename="../../dialog.ui" line="599"/>
<source>terminate</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="586"/>
<source>execute</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="612"/>
<source>clear</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="272"/>
<source>reverse connection</source>
<translation></translation>
</message>
<message>
<source>auto enable</source>
<translation type="vanished"></translation>
</message>
<message>
<location filename="../../dialog.ui" line="256"/>
<source>background record</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="220"/>
<source>screen-off</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="189"/>
<source>apply</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="85"/>
<source>max size:</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="240"/>
<source>always on top</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="182"/>
<source>refresh script</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="403"/>
<source>get device IP</source>
<translation>IP</translation>
</message>
<message>
<location filename="../../dialog.ui" line="302"/>
<source>USB line</source>
<translation>USB线</translation>
</message>
<message>
<location filename="../../dialog.ui" line="358"/>
<source>stop server</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="348"/>
<source>start server</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="338"/>
<source>device serial:</source>
<translation></translation>
</message>
<message>
<source>Config</source>
<translation type="vanished"></translation>
</message>
<message>
<location filename="../../dialog.ui" line="68"/>
<source>bit rate:</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.ui" line="413"/>
<source>start adbd</source>
<translation>adbd</translation>
</message>
<message>
<location filename="../../dialog.ui" line="393"/>
<source>refresh devices</source>
<translation></translation>
</message>
<message>
<location filename="../../dialog.cpp" line="101"/>
<source>original</source>
<translation></translation>
</message>
</context>
<context>
<name>QObject</name>
<message>
<source>This software is completely open source and free, you can download it at the following address:</source>
<translation type="vanished"></translation>
</message>
<message>
<source>This software is completely open source and free.
Strictly used for illegal purposes, or at your own risk.
You can download it at the following address:</source>
<translation type="vanished">.\n严禁用于非法用途.\n你可以在下面地址下载:</translation>
</message>
<message>
<location filename="../../main.cpp" line="103"/>
<source>This software is completely open source and free. Strictly used for illegal purposes, or at your own risk. You can download it at the following address:</source>
<translation>:</translation>
</message>
</context>
<context>
<name>ToolForm</name>
<message>
<location filename="../../device/ui/toolform.ui" line="14"/>
<source>Tool</source>
<translation></translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="33"/>
<source>full screen</source>
<translation></translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="56"/>
<source>expand notify</source>
<translation></translation>
</message>
<message>
<source>turn off</source>
<translation type="vanished"></translation>
</message>
<message>
<source>turn on</source>
<translation type="vanished"></translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="66"/>
<source>touch switch</source>
<translation></translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="76"/>
<source>close screen</source>
<translation></translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="86"/>
<source>power</source>
<translation></translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="96"/>
<source>volume up</source>
<translation></translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="106"/>
<source>volume down</source>
<translation></translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="116"/>
<source>app switch</source>
<translation></translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="126"/>
<source>menu</source>
<translation></translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="136"/>
<source>home</source>
<translation></translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="146"/>
<source>return</source>
<translation></translation>
</message>
<message>
<location filename="../../device/ui/toolform.ui" line="156"/>
<source>screen shot</source>
<translation></translation>
</message>
</context>
<context>
<name>VideoForm</name>
<message>
<source>wait current file transfer to complete</source>
<translation type="vanished"></translation>
</message>
<message>
<source>file transfer complete</source>
<translation type="vanished"></translation>
</message>
<message>
<source>file transfer failed</source>
<translation type="vanished"></translation>
</message>
<message>
<location filename="../../device/ui/videoform.cpp" line="671"/>
<source>file does not exist</source>
<translation></translation>
</message>
</context>
<context>
<name>videoForm</name>
<message>
<source>qrc:/qml/pinwheel.qml</source>
<translation type="vanished">qrc:/qml/pinwheel.qml</translation>
</message>
</context>
</TS>

Binary file not shown.

Binary file not shown.

View file

@ -1,321 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
<context>
<name>Dialog</name>
<message>
<source>show</source>
<translation></translation>
</message>
<message>
<source>quit</source>
<translation>退</translation>
</message>
<message>
<source>original</source>
<translation></translation>
</message>
<message>
<source>no lock</source>
<translation></translation>
</message>
<message>
<source>Notice</source>
<translation></translation>
</message>
<message>
<source>Hidden here!</source>
<translation></translation>
</message>
<message>
<source>select path</source>
<translation></translation>
</message>
<message>
<source>Clear History</source>
<translation></translation>
</message>
</context>
<context>
<name>QObject</name>
<message>
<source>This software is completely open source and free. Use it at your own risk. You can download it at the following address:</source>
<translation>使</translation>
</message>
<message>
<source>QuickMirror</source>
<translation></translation>
</message>
<message>
<source>If you need more professional batch control mirror software, you can try the following software:</source>
<translation></translation>
</message>
<message>
<source>If you need more professional game keymap mirror software, you can try the following software:</source>
<translation></translation>
</message>
<message>
<source>QuickAssistant</source>
<translation></translation>
</message>
<message>
<source>You can contact me with telegram &lt;https://t.me/+Ylf_5V_rDCMyODQ1&gt;</source>
<translation>QQ群联系我 &lt;901736468&gt;</translation>
</message>
</context>
<context>
<name>ToolForm</name>
<message>
<source>Tool</source>
<translation></translation>
</message>
<message>
<source>full screen</source>
<translation></translation>
</message>
<message>
<source>expand notify</source>
<translation></translation>
</message>
<message>
<source>touch switch</source>
<translation></translation>
</message>
<message>
<source>close screen</source>
<translation></translation>
</message>
<message>
<source>power</source>
<translation></translation>
</message>
<message>
<source>volume up</source>
<translation></translation>
</message>
<message>
<source>volume down</source>
<translation></translation>
</message>
<message>
<source>app switch</source>
<translation></translation>
</message>
<message>
<source>menu</source>
<translation></translation>
</message>
<message>
<source>home</source>
<translation></translation>
</message>
<message>
<source>return</source>
<translation></translation>
</message>
<message>
<source>screen shot</source>
<translation></translation>
</message>
<message>
<source>open screen</source>
<translation></translation>
</message>
<message>
<source>group control</source>
<translation></translation>
</message>
</context>
<context>
<name>VideoForm</name>
<message>
<source>file does not exist</source>
<translation></translation>
</message>
</context>
<context>
<name>Widget</name>
<message>
<source>Wireless</source>
<translation>线</translation>
</message>
<message>
<source>wireless connect</source>
<translation>线</translation>
</message>
<message>
<source>wireless disconnect</source>
<translation>线</translation>
</message>
<message>
<source>Start Config</source>
<translation></translation>
</message>
<message>
<source>select path</source>
<translation></translation>
</message>
<message>
<source>record format</source>
<translation></translation>
</message>
<message>
<source>record screen</source>
<translation></translation>
</message>
<message>
<source>frameless</source>
<translation></translation>
</message>
<message>
<source>Use Simple Mode</source>
<translatorcomment></translatorcomment>
<translation></translation>
</message>
<message>
<source>Simple Mode</source>
<translatorcomment></translatorcomment>
<translation></translation>
</message>
<message>
<source>WIFI Connect</source>
<translatorcomment>WIFI连接</translatorcomment>
<translation>WIFI连接</translation>
</message>
<message>
<source>USB Connect</source>
<translatorcomment>USB连接</translatorcomment>
<translation>USB连接</translation>
</message>
<message>
<source>Double click to connect:</source>
<translation></translation>
</message>
<message>
<source>lock orientation:</source>
<translation></translation>
</message>
<message>
<source>show fps</source>
<translation>fps</translation>
</message>
<message>
<source>stay awake</source>
<translation></translation>
</message>
<message>
<source>device name:</source>
<translatorcomment>:</translatorcomment>
<translation>:</translation>
</message>
<message>
<source>update name</source>
<translatorcomment></translatorcomment>
<translation></translation>
</message>
<message>
<source>stop all server</source>
<translation></translation>
</message>
<message>
<source>adb command:</source>
<translation>adb命令</translation>
</message>
<message>
<source>terminate</source>
<translation></translation>
</message>
<message>
<source>execute</source>
<translation></translation>
</message>
<message>
<source>clear</source>
<translation></translation>
</message>
<message>
<source>reverse connection</source>
<translation></translation>
</message>
<message>
<source>background record</source>
<translation></translation>
</message>
<message>
<source>screen-off</source>
<translation></translation>
</message>
<message>
<source>apply</source>
<translation></translation>
</message>
<message>
<source>max size:</source>
<translation></translation>
</message>
<message>
<source>always on top</source>
<translation></translation>
</message>
<message>
<source>refresh script</source>
<translation></translation>
</message>
<message>
<source>get device IP</source>
<translation>IP</translation>
</message>
<message>
<source>USB line</source>
<translation>USB线</translation>
</message>
<message>
<source>stop server</source>
<translation></translation>
</message>
<message>
<source>start server</source>
<translation></translation>
</message>
<message>
<source>device serial:</source>
<translation></translation>
</message>
<message>
<source>bit rate:</source>
<translation></translation>
</message>
<message>
<source>start adbd</source>
<translation>adbd</translation>
</message>
<message>
<source>refresh devices</source>
<translation></translation>
</message>
<message>
<source>install sndcpy</source>
<translation>sndcpy</translation>
</message>
<message>
<source>start audio</source>
<translation></translation>
</message>
<message>
<source>stop audio</source>
<translation></translation>
</message>
<message>
<source>auto update</source>
<translation></translation>
</message>
<message>
<source>show toolbar</source>
<translation></translation>
</message>
<message>
<source>record save path:</source>
<translation></translation>
</message>
</context>
</TS>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View file

@ -2,7 +2,7 @@
<qresource prefix="/">
<file>font/fontawesome-webfont.ttf</file>
<file>image/videoform/phone-h.png</file>
<file>image/videoform/phone-v.png</file>
<file>image/videoform/phone-v.png</file>
<file>qss/psblack.css</file>
<file>qss/psblack/add_bottom.png</file>
<file>qss/psblack/add_left.png</file>
@ -22,8 +22,7 @@
<file>qss/psblack/radiobutton_checked_disable.png</file>
<file>qss/psblack/radiobutton_unchecked.png</file>
<file>qss/psblack/radiobutton_unchecked_disable.png</file>
<file>i18n/en_US.qm</file>
<file>i18n/zh_CN.qm</file>
<file>image/tray/logo.png</file>
<file>i18n/QtScrcpy_en.qm</file>
<file>i18n/QtScrcpy_zh.qm</file>
</qresource>
</RCC>

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more