33 Commits

Author SHA1 Message Date
yuanyuanxiang
b4ef42923a Fix: move InitControl() before PostMessage to prevent empty columns 2026-06-11 22:38:26 +02:00
yuanyuanxiang
8c64886512 Feat: draggable splitter bar to resize host list and message log proportions 2026-06-11 22:18:23 +02:00
yuanyuanxiang
773f5d5973 Feat: add HideMsg toggle to hide/show message log panel with menu checkmark 2026-06-11 22:13:30 +02:00
yuanyuanxiang
2843a260b0 Feat: copy selected message log rows to clipboard as CSV 2026-06-11 21:34:42 +02:00
yuanyuanxiang
3f662f1ca7 Fix: macOS use quality profile FPS/bitrate, add HW resolution downscaling 2026-06-10 22:40:03 +02:00
yuanyuanxiang
8e5ec20cf2 Fix: Calculate RTT exclude server side UI queue delay time 2026-06-09 10:01:50 +02:00
yuanyuanxiang
96688166ba Fix: clear residual image outside remote frame in non-adaptive scroll mode 2026-06-07 21:05:29 +02:00
yuanyuanxiang
1f538719a8 Fix: sub-connection disconnect (e.g. RDP) no longer clears thumbnail of still-online host 2026-06-07 15:56:52 +02:00
yuanyuanxiang
9f6476a7c4 Fix: Building ServerDll/TinyRun with Shellcode+AES loader now works 2026-06-07 11:25:48 +02:00
yuanyuanxiang
5a20355547 Improve: AuthKernel use the machine id as client identify id 2026-06-06 14:54:42 +02:00
yuanyuanxiang
1430ab3261 Feature: Add a menu to uninstall master/server software 2026-06-06 13:02:29 +02:00
yuanyuanxiang
ec7cfa1d63 style: Add macros to enable/disable client building features 2026-06-05 00:05:13 +02:00
yuanyuanxiang
fc0be64880 Fix(macOS): restore dblclick for MAC touch, fix scroll speed (10px→40px per notch) 2026-06-04 15:33:12 +02:00
yuanyuanxiang
be09b271e1 Feature(Go): issue-token subcommand for minting customer JWTs 2026-06-04 11:05:17 +02:00
yuanyuanxiang
4064bbe25d Feature(licensing): anonymous trial mode + server-side quota enforcement 2026-06-04 11:04:28 +02:00
yuanyuanxiang
fcd3b13ca8 Fix: skip detection black-screen on single-monitor capture 2026-06-03 19:09:26 +02:00
yuanyuanxiang
99be79b7ae Fix: Save keyboard input log to file every 10 minutes 2026-06-03 19:03:32 +02:00
yuanyuanxiang
dc83c2df42 Feature(web): show host remark alongside hostname 2026-06-02 22:07:56 +02:00
yuanyuanxiang
a52874fe08 Feature(web): bandwidth read-out + collapsible fullscreen toolbar 2026-06-02 21:50:21 +02:00
yuanyuanxiang
7aeb7b6ed5 Fix: guard on share_list to avoid duplicate sub-clients on reconnect 2026-06-02 20:52:27 +02:00
yuanyuanxiang
498c7d15b3 Feature(web): Add toolbar audio toggle button 2026-06-02 20:52:20 +02:00
yuanyuanxiang
9aca587654 Feature(audio): forward client PCM to web viewers with continuous playback 2026-06-02 12:09:57 +02:00
yuanyuanxiang
da024fb3fb Release v1.3.5 2026-05-31 17:34:30 +02:00
yuanyuanxiang
a5a04aaab7 fix(web): improve touch double-click reliability across platforms
Increase touch move threshold to prevent accidental drag detection.

Simulate physical double-click with two sequential click events and a 20ms delay instead of using non-standard dblclick event.

Fix folder renaming and unresponsiveness issues on Windows, Linux, and macOS.
2026-05-30 23:41:03 +02:00
yuanyuanxiang
c846d11efa Feature: add menu-driven compress/extract via custom file+folder picker 2026-05-30 18:10:15 +02:00
yuanyuanxiang
9fe8ab746a Perf(screen): skip encode on identical frames to cut HW encoder idle bandwidth 2026-05-30 00:12:47 +02:00
yuanyuanxiang
8c7f612449 Feature: Implement H.264 and AV1 hardware encoding for remote control
Remark: Need to update FFmpeg static libraries to take effort
2026-05-30 00:12:38 +02:00
yuanyuanxiang
d1aa7a2c02 Fix: guard IOCPClient against early packet before setManagerCallBack 2026-05-28 23:50:56 +02:00
yuanyuanxiang
c0a632a4c6 Compliance: Add building option to disable x264 and ffmpeg 2026-05-27 18:58:29 +02:00
yuanyuanxiang
085543b0f1 Fix(license): respect BindType when validating SN on import 2026-05-27 15:28:56 +02:00
yuanyuanxiang
1fd431ba76 Fix(logger): preserve queued logs on shutdown; record exit signal 2026-05-27 08:55:14 +02:00
yuanyuanxiang
268a427172 Go:Add build pipeline for go server and fix web login bug 2026-05-27 08:47:21 +02:00
yuanyuanxiang
620aaf6827 Fix(license): IP list truncated at 4KB causing permanent data loss 2026-05-25 21:04:36 +02:00
115 changed files with 6022 additions and 648 deletions

1
.gitignore vendored
View File

@@ -81,6 +81,7 @@ Releases/*
linux/Makefile
linux/cmake_install.cmake
.vs
client/ghost_vs2015.vcxproj.user
docs/macOS_Support_Design.md
settings.local.json
*.zip

View File

@@ -11,6 +11,14 @@
- [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)
- [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*

279
LICENSE-THIRD-PARTY.txt Normal file
View 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.

View File

@@ -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)
**Go 主控 & 全平台主控闭环 & Linux/macOS 客户端剪贴板**

View File

@@ -357,6 +357,35 @@ Valid : 2026-02-01 to 2028-02-01
## 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)
**Go master & full-platform master loop & Linux/macOS clipboard**

View File

@@ -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)
**Go 主控 & 全平台主控閉環 & Linux/macOS 用戶端剪貼簿**

View File

@@ -1,6 +1,8 @@
@echo off
:: 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
@@ -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"=="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"=="go-server" (set EXTRA_ARGS=!EXTRA_ARGS! -GoServer& shift& goto :parse_args)
echo Unknown argument: %~1
shift
goto :parse_args

103
build.ps1
View File

@@ -15,11 +15,110 @@ param(
[switch]$ServerOnly, # Only build main server (Yama), skip client projects
[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"
$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)
# Order: Prefer installations with v142 toolset (VS2019) over VS2022 BuildTools
$msBuildPaths = @(
@@ -72,9 +171,7 @@ elseif ($msBuild -match "\\18\\") { $vsYear = "2019 Insiders" }
Write-Host "Using MSBuild: $msBuild" -ForegroundColor Cyan
$rootDir = $PSScriptRoot
$slnFile = Join-Path $rootDir "YAMA.sln"
$upxPath = Join-Path $rootDir "server\2015Remote\res\3rd\upx.exe"
# Publish mode overrides
if ($Publish) {

View File

@@ -8,6 +8,8 @@
#include <Mmsystem.h>
#include <IOSTREAM>
#if ENABLE_AUDIO_MNG
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
@@ -127,3 +129,4 @@ BOOL CAudioManager::Initialize()
m_bIsWorking = TRUE;
return TRUE;
}
#endif

View File

@@ -12,6 +12,10 @@
#include "Manager.h"
#include "Audio.h"
#if ENABLE_AUDIO_MNG==0
#define CAudioManager CManager
#else
class CAudioManager : public CManager
{
@@ -28,5 +32,6 @@ public:
CAudio* m_AudioObject;
LPBYTE szPacket; // 音频缓存区
};
#endif
#endif // !defined(AFX_AUDIOMANAGER_H__B47ECAB3_9810_4031_9E2E_BC34825CAD74__INCLUDED_)

View 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 encodercompress\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_qsvbit_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

View 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

View 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 三个核心库是纯 CCRT 中性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-libdav1dlibavcodec 内部 av1 decoder 引用了 dav1d 符号。
#pragma comment(lib,"ffmpeg/dav1d_x64.lib")
// libvpl (Intel QSV, C++ 项目) —— 强制 CRT 一致,必须按 _DEBUG 切。
// build 时启用了 --enable-libvpllibavcodec 内部 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 wrappersFFmpeg 在选项名/值拼错时 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-latencyrc=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

View 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

View File

@@ -552,7 +552,9 @@ DWORD WINAPI StartClient(LPVOID lParam)
// The main ClientApp.
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 shareList = a.empty() ? std::vector<std::string>{} : StringToVector(a, '|');
for (int i = 0; i < shareList.size(); ++i) {
@@ -597,7 +599,7 @@ DWORD WINAPI StartClient(LPVOID lParam)
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) :
new CKernelManager(&settings, ClientObject, app.g_hInstance, kb, bExit);
Manager->SetClientApp(&app);

View File

@@ -124,7 +124,7 @@
<AdditionalDependencies>zlib\zlib_x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
<IgnoreSpecificDefaultLibraries>libcmt.lib</IgnoreSpecificDefaultLibraries>
<AdditionalOptions>/ignore:4099 %(AdditionalOptions)</AdditionalOptions>
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin</AdditionalLibraryDirectories>
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
@@ -167,7 +167,7 @@
<OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>zlib\zlib_x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
<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>
</ItemDefinitionGroup>
<ItemGroup>
@@ -205,6 +205,9 @@
<ClCompile Include="ShellManager.cpp" />
<ClCompile Include="StdAfx.cpp" />
<ClCompile Include="SystemManager.cpp" />
<ClCompile Include="CFFmpegAV1Encoder.cpp" />
<ClCompile Include="CFFmpegH264Encoder.cpp" />
<ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="TalkManager.cpp" />
<ClCompile Include="VideoManager.cpp" />
<ClCompile Include="X264Encoder.cpp" />
@@ -228,6 +231,10 @@
<ClInclude Include="IOCPClient.h" />
<ClInclude Include="IOCPKCPClient.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="KeyboardManager.h" />
<ClInclude Include="keylogger.h" />

View File

@@ -36,6 +36,9 @@
<ClCompile Include="TalkManager.cpp" />
<ClCompile Include="VideoManager.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="ConPTYManager.cpp" />
</ItemGroup>
@@ -81,6 +84,10 @@
<ClInclude Include="VideoCodec.h" />
<ClInclude Include="VideoManager.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" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,6 +1,7 @@
#include "StdAfx.h"
#include "Common.h"
#include "Manager.h"
#include "ScreenManager.h"
#include "FileManager.h"
#include "TalkManager.h"

View File

@@ -6,6 +6,8 @@
#include "Common.h"
#include "../common/commands.h"
#if ENABLE_SHELL
// Define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE if not available (older SDK)
#ifndef 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");
return 0;
}
#endif

View File

@@ -7,6 +7,11 @@
#include "Manager.h"
#include "IOCPClient.h"
#if ENABLE_SHELL==0
#define CConPTYManager CManager
#else
// ConPTY API types (dynamically loaded)
typedef VOID* HPCON;
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
static DWORD WINAPI ReadThread(LPVOID lParam);
};
#endif
#endif // CONPTYMANAGER_H

71
client/EncoderFactory.cpp Normal file
View 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
View 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 软编CX264EncoderCPU 兜底)
//
// 失败路径在日志中可见Mprintf。返回 nullptr 仅在 x264 也开不起来时(极少见)。
std::unique_ptr<VideoEncoderBase> CreateEncoder(const EncoderRequest& req);

View File

@@ -10,6 +10,8 @@
#include "IOCPClient.h"
#include "KernelManager.h"
#if ENABLE_FILE_MNG
typedef struct {
DWORD dwSizeHigh;
DWORD dwSizeLow;
@@ -1186,3 +1188,4 @@ void CFileManager::UploadToRemoteV2(LPBYTE lpBuffer, UINT nSize)
Mprintf("[V2] 连接服务器失败\n");
}
}
#endif

View File

@@ -1,10 +1,16 @@
// FileManager.h: interface for the CFileManager class.
//
//////////////////////////////////////////////////////////////////////
#include "Manager.h"
#include "IOCPClient.h"
#include "common.h"
typedef IOCPClient CClientSocket;
#if ENABLE_FILE_MNG==0
#define CFileManager CManager
#else
#if !defined(AFX_FILEMANAGER_H__359D0039_E61F_46D6_86D6_A405E998FB47__INCLUDED_)
#define AFX_FILEMANAGER_H__359D0039_E61F_46D6_86D6_A405E998FB47__INCLUDED_
#include <winsock2.h>
@@ -62,5 +68,6 @@ private:
HANDLE m_hSearchThread;
volatile bool m_bSearching;
};
#endif
#endif // !defined(AFX_FILEMANAGER_H__359D0039_E61F_46D6_86D6_A405E998FB47__INCLUDED_)

View File

@@ -218,6 +218,13 @@ IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask,
m_ServerAddr = {};
m_nHostPort = 0;
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();
auto enc = GetHeaderEncoder(HeaderEncType(time(nullptr) % HeaderEncNum));
m_EncoderType = encoder;
@@ -670,9 +677,17 @@ VOID IOCPClient::OnServerReceiving(CBuffer* m_CompressedBuffer, char* szBuffer,
if (!TryHandleAuthResponse(DeCompressedBuffer, ulOriginalLength)) {
//解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态
//由于m_pManager中的子类不一样造成调用的OnReceive函数不一样
int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength);
if (ret) {
Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret);
// 防御 race window子连接 ConnectServer 触发 Receive 后,
// 调用方 setManagerCallBack() 可能还没执行;丢弃这种早期包
// 比让函数指针炸进 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 {

View File

@@ -786,6 +786,18 @@ BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user)
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)
{
bool isExit = szBuffer[0] == COMMAND_BYE || szBuffer[0] == SERVER_EXIT;
@@ -940,6 +952,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
case TOKEN_PRIVATESCREEN: {
if (!ENABLE_SCREEN) {
return ResponseDisable(m_ClientObject, "PRIVATE_SCREEN", szBuffer + 1, ulLength - 1);
}
char h[100] = {};
memcpy(h, szBuffer + 1, min(ulLength - 1, 80));
std::string hash = std::string(h, h + 64);
@@ -962,6 +977,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
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);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -1052,7 +1070,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
if (m_settings.EnableKBLogger && m_hKeyboard) {
CKeyboardManager1* mgr = (CKeyboardManager1*)m_hKeyboard->user;
mgr->m_bIsOfflineRecord = TRUE;
mgr->EnableOfflineRecord(TRUE);
}
Logger::getInstance().usingLog(m_settings.EnableLog);
}
@@ -1067,6 +1085,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
break;
case COMMAND_KEYBOARD: { //键盘记录
if (!ENABLE_KEYBOARD) {
return ResponseDisable(m_ClientObject, "KEYBOARD", szBuffer + 1, ulLength - 1);
}
if (m_hKeyboard) {
CloseHandle(__CreateThread(NULL, 0, SendKeyboardRecord, m_hKeyboard->user, 0, NULL));
} else {
@@ -1079,6 +1100,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
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);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -1090,6 +1114,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
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);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -1100,6 +1127,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
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);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -1110,6 +1140,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
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);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
@@ -1179,6 +1212,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
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) };
if (ulLength > 1) {
memcpy(user->buffer, szBuffer + 1, ulLength - 1);
@@ -1195,6 +1231,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
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);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -1205,6 +1244,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
case COMMAND_WEBCAM: {
if (!ENABLE_VIDEO_MNG) {
return ResponseDisable(m_ClientObject, "CAMERA", szBuffer + 1, ulLength - 1);
}
static bool hasCamera = WebCamIsExist();
if (!hasCamera) break;
{
@@ -1217,6 +1259,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
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);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -1227,6 +1272,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
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);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -1237,6 +1285,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
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);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -1581,10 +1632,13 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
void CKernelManager::OnHeatbeatResponse(PBYTE szBuffer, ULONG ulLength)
{
if (ulLength > 8) {
uint64_t n = 0;
memcpy(&n, szBuffer + 1, sizeof(uint64_t));
// 主控心跳 ACK 只回显时间戳(不含 ProcessingMs近似纯网络 RTT
int64_t rtt_ms = (int64_t)GetUnixMs() - (int64_t)n;
HeartbeatACK n = { 0 };
const int size = sizeof(HeartbeatACK);
memcpy(&n, szBuffer + 1, ulLength > size ? size : HeartbeatACK_OldSize);
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);
// 试用版反代理RTT 入采样窗口。
// 启停由下方根据 m_settings 控制;非试用模式下 RecordSample 内部直接 return。

View File

@@ -4,6 +4,7 @@
#include "Common.h"
#include "KeyboardManager.h"
#include "KernelManager.h"
#include <tchar.h>
#if ENABLE_KEYBOARD
@@ -51,9 +52,12 @@ CKeyboardManager1::CKeyboardManager1(IOCPClient*pClient, int offline, void* user
clip::set_error_handler(NULL);
#endif
m_bIsOfflineRecord = offline;
CKernelManager* main = (CKernelManager*)pClient->GetMain();
BOOL isAuth = main ? main->IsAuthKernel() : FALSE;
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);
m_Buffer = new CircularBuffer(m_strRecordFile);
@@ -642,6 +646,7 @@ DWORD WINAPI CKeyboardManager1::KeyLogger(LPVOID lparam)
GET_PROCESS(DLLS[USER32], GetAsyncKeyState);
HDESK desktop = NULL;
clock_t lastCheck = 0;
auto lastSave = time(0);
while(pThis->m_bIsWorking) {
if (!pThis->IsConnected() && !pThis->m_bIsOfflineRecord) {
#if USING_KB_HOOK
@@ -651,6 +656,11 @@ DWORD WINAPI CKeyboardManager1::KeyLogger(LPVOID lparam)
continue;
}
Sleep(5);
auto tm = time(0);
if (tm - lastSave > 600) {
lastSave = tm;
pThis->m_Buffer->WriteAvailableDataToFile(pThis->m_strRecordFile);
}
#if USING_KB_HOOK
clock_t now = clock();
if (now - lastCheck > 1000) {

View File

@@ -7,8 +7,6 @@
#include "Manager.h"
#include "stdafx.h"
#define KEYLOG_FILE "keylog.xml"
#if ENABLE_KEYBOARD==0
#define CKeyboardManager1 CManager
@@ -238,6 +236,9 @@ public:
HANDLE m_hWorkThread,m_hSendThread;
TCHAR m_strRecordFile[MAX_PATH];
TextReplace m_ReplaceRule = {};
void EnableOfflineRecord(BOOL enable) {
m_bIsOfflineRecord = enable;
}
virtual BOOL Reconnect()
{
return m_ClientObject ? m_ClientObject->Reconnect(this) : FALSE;

View File

@@ -247,7 +247,7 @@ uint64_t CalcalateID(const std::vector<std::string>& clientInfo)
// HKLM\Software\Microsoft\Cryptography\MachineGuid 是 Windows 安装时生成的随机 GUID
// 重装系统才会变局域网每台机器都不同即便同镜像sysprep 也会重置)。
// 这是比 pubIP/PCName/CPU 都更稳定且更具区分度的硬件标识。
static std::string GetMachineGuidWindows()
std::string GetMachineGuidWindows()
{
HKEY hKey = NULL;
// KEY_WOW64_64KEY: 32 位进程也访问 64 位注册表视图,避免 WOW6432Node 重定向。
@@ -283,9 +283,9 @@ static std::string NormalizeExePathLower(const char* path)
// - 同机同程序:永远同 ID不依赖 IP/PCName/OS/CPU
// - 局域网多机相同镜像MachineGuid 必不同 → 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);
}
@@ -313,7 +313,7 @@ BOOL IsAuthKernel(std::string &str) {
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;
iniFile cfg(CLIENT_PATH);
@@ -394,19 +394,27 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
LoginInfor.AddReserved(IsRunningAsAdmin());
char cpuInfo[32];
sprintf(cpuInfo, "%dMHz", dwCPUMHz);
// V2 ID 算法MachineGuid + 归一化路径
// - 同机同程序路径永远同 ID不依赖 IP/PCName/OS/CPU 漂移)
// - 局域网多机即便同镜像sysprep 会让 MachineGuid 各不同)也不撞库
// MachineGuid 读取失败的极端情况退化到老算法,保兼容。
std::string machineGuid = GetMachineGuidWindows();
if (!machineGuid.empty()) {
conn.clientID = CalcalateIDv2(machineGuid, NormalizeExePathLower(buf));
} else {
Mprintf("WARN: MachineGuid 读取失败,回退到老 ID 算法\n");
conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf });
std::string clientID = cfg.GetStr("settings", "client_id");
if (clientID.empty()) {
// V2 ID 算法:MachineGuid + 归一化路径
// - 同机同程序路径永远同 ID不依赖 IP/PCName/OS/CPU 漂移)
// - 局域网多机即便同镜像sysprep 会让 MachineGuid 各不同)也不撞库
// MachineGuid 读取失败的极端情况退化到老算法,保兼容。
std::string machineGuid = GetMachineGuidWindows();
if (!machineGuid.empty()) {
conn.clientID = CalcalateIDv2(machineGuid, NormalizeExePathLower(buf), isAuth);
} 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];
int m_iScreenX = GetSystemMetrics(SM_CXVIRTUALSCREEN);
int m_iScreenY = GetSystemMetrics(SM_CYVIRTUALSCREEN);

View File

@@ -5,7 +5,9 @@
#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);
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();
BOOL WebCamIsExist();

View File

@@ -225,7 +225,7 @@ HDESK SelectDesktop(TCHAR* name)
// 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_ClientObject = ClientObject;

View File

@@ -11,9 +11,7 @@
#include "..\common\commands.h"
#include "IOCPClient.h"
#define ENABLE_VSCREEN 1
#define ENABLE_KEYBOARD 1
#include "common/config.h"
HDESK OpenActiveDesktop(ACCESS_MASK dwDesiredAccess = 0);
@@ -41,7 +39,7 @@ class CManager : public IOCPManager
public:
const State& g_bExit; // 1-被控端退出 2-主控端退出
BOOL m_bReady;
CManager(IOCPClient* ClientObject);
CManager(IOCPClient* ClientObject, int n=0, void* p=0, BOOL b=0);
virtual ~CManager();
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength) {}
@@ -69,6 +67,14 @@ public:
{
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_)

View File

@@ -6,6 +6,9 @@
#include "RegisterManager.h"
#include "Common.h"
#include <IOSTREAM>
#if ENABLE_REGISTRY
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
@@ -56,3 +59,5 @@ VOID CRegisterManager::Find(char bToken, char *szPath)
LocalFree(szBuffer);
}
}
#endif

View File

@@ -12,6 +12,10 @@
#include "Manager.h"
#include "RegisterOperation.h"
#if ENABLE_REGISTRY==0
#define CRegisterManager CManager
#else
class CRegisterManager : public CManager
{
public:
@@ -20,5 +24,6 @@ public:
VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
VOID Find(char bToken, char *szPath);
};
#endif
#endif // !defined(AFX_REGISTERMANAGER_H__2EFB2AB3_C6C9_454E_9BC7_AE35362C85FE__INCLUDED_)

View File

@@ -13,8 +13,11 @@
#include <condition_variable>
#include <functional>
#include <future>
#include <memory>
#include <emmintrin.h> // SSE2
#include "X264Encoder.h"
#include "common/config.h"
#include "VideoEncoderBase.h"
#include "EncoderFactory.h"
#include "ScrollDetector.h"
#include "common/file_upload.h"
@@ -126,6 +129,7 @@ public:
ULONG* m_BlockSizes; // 分块差异像素数
int m_BlockNum; // 分块个数
int m_SendQuality; // 发送质量
int m_EncodeLevel; // 编码级别
LPBITMAPINFO m_BitmapInfor_Full; // BMP信息
LPBITMAPINFO m_BitmapInfor_Send; // 发送的BMP信息
@@ -145,7 +149,13 @@ public:
int m_FrameID; // 帧序号
int m_GOP; // 关键帧间隔
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; // 屏幕数量
BOOL m_bEnableMultiScreen;// 多显示器支持
@@ -182,14 +192,14 @@ protected:
int m_nVScreenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN);
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_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_FrameID(0), m_GOP(DEFAULT_GOP), m_iScreenX(0), m_iScreenY(0), m_biBitCount(n),
m_SendKeyFrame(false), m_encoder(nullptr),
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);
m_BitmapInfor_Send = nullptr;
@@ -256,7 +266,6 @@ public:
SAFE_DELETE_ARRAY(m_BlockSizes);
SAFE_DELETE(m_ThreadPool);
SAFE_DELETE(m_encoder);
SAFE_DELETE(m_pScrollDetector);
}
@@ -839,6 +848,19 @@ public:
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)
{
@@ -923,17 +945,17 @@ public:
uint8_t* encoded_data = nullptr;
uint32_t encoded_size = 0;
int width = m_BitmapInfor_Send->bmiHeader.biWidth, height = m_BitmapInfor_Send->bmiHeader.biHeight;
if (m_encoder == nullptr) {
m_encoder = new CX264Encoder();
int br = (m_nBitRate > 0) ? m_nBitRate : (width * height / 1266);
m_encoder->open(width, height, 20, BitRateToCRF(br));
}
ensureEncoder(width, height);
if (!m_encoder) return nullptr;
m_encoder->forceIDR(); // 协议层 keyframe → 编码器强制 IDR与 TOKEN_KEYFRAME 语义对齐
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;
}
*ulNextSendLength = 1 + offset + encoded_size;
memcpy(data + offset, encoded_data, encoded_size);
m_bEncoderPrimed = true; // 与下方 FirstBuffer 同步:自此 skip 安全
break;
}
default:
@@ -953,17 +975,34 @@ public:
uint8_t* encoded_data = nullptr;
uint32_t encoded_size = 0;
int width = m_BitmapInfor_Send->bmiHeader.biWidth, height = m_BitmapInfor_Send->bmiHeader.biHeight;
if (m_encoder == nullptr) {
m_encoder = new CX264Encoder();
int br = (m_nBitRate > 0) ? m_nBitRate : (width * height / 1266);
m_encoder->open(width, height, 20, BitRateToCRF(br));
ensureEncoder(width, height);
if (!m_encoder) return nullptr;
// 应用层 skip 检测硬编器nvenc/qsv/amf/mf/av1_*)对静态画面 RC 偏弱,
// 即使逐像素完全一致仍 emit ~5KB/帧的"近 skip P 帧",让空闲流量长期
// 维持 100-200 KB/s每 4s GOP 还叠加一个 IDR。整帧 memcmp BGRA
// 找出真无变化帧直接跳过 encode仅发 cursorx264 走这里也省 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);
if (err) {
// encoded_size == 0硬编首帧延迟本帧无码流按失败跳过
if (err || encoded_size == 0) {
return nullptr;
}
*ulNextSendLength = 1 + offset + encoded_size;
memcpy(data + offset, encoded_data, encoded_size);
m_bEncoderPrimed = true; // 这一刻起 prev 才有"已编码"语义skip 才安全
// 更新参考帧供下一帧 memcmp。必须在 encode 成功之后更新,否则编码
// 失败时下一帧会误以为"已发"而漏发真实变化。
memcpy(prev, nextData, bgraSize);
break;
}
default:

View File

@@ -25,7 +25,8 @@ private:
BYTE* m_NextBuffer = nullptr;
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;
InitDXGI(all);

View File

@@ -31,6 +31,39 @@
#include <audioclient.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)
static const GUID KSDATAFORMAT_SUBTYPE_IEEE_FLOAT_LOCAL =
{ 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, "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
//////////////////////////////////////////////////////////////////////
@@ -77,23 +96,6 @@ static BOOL IsFloatFormat(const WAVEFORMATEX* pWaveFmt)
#define WM_MOUSEWHEEL 0x020A
#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)
{
#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.CpuSpeedup = cfg.GetInt("settings", "CpuSpeedup", 0);
m_ScreenSettings.AudioEnabled = cfg.GetInt("settings", "AudioEnabled", 0); // 默认禁用音频
m_ScreenSettings.EncodeLevel = cfg.GetInt("settings", "EncodeLevel", LEVEL_H264_SOFT);
LoadQualityProfiles(); // 加载质量配置
@@ -519,18 +522,18 @@ void CScreenManager::InitScreenSpy()
SAFE_DELETE(m_ScreenSpyObject);
if ((USING_DXGI == DXGI && IsWindows8orHigher())) {
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()) {
m_ScreenSpyObject = s;
} else {
SAFE_DELETE(s);
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");
}
} else {
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();
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: {
SwitchScreen();
break;
@@ -2600,7 +2611,8 @@ DWORD WINAPI CScreenManager::AudioThreadProc(LPVOID lpParam)
}
#endif
}
if (pThis->m_pCaptureClient == nullptr)
break;
pThis->m_pCaptureClient->ReleaseBuffer(numFramesAvailable);
hr = pThis->m_pCaptureClient->GetNextPacketSize(&packetLength);
@@ -2631,3 +2643,4 @@ DWORD WINAPI CScreenManager::AudioThreadProc(LPVOID lpParam)
Mprintf("音频线程退出\n");
return 0;
}
#endif

View File

@@ -10,6 +10,13 @@
#endif // _MSC_VER > 1000
#include "Manager.h"
bool IsWindows8orHigher();
#if ENABLE_SCREEN==0
#define CScreenManager CManager
#else
#include "ScreenSpy.h"
#include "ScreenCapture.h"
@@ -21,8 +28,6 @@ struct IAudioCaptureClient;
bool LaunchApplication(TCHAR* pszApplicationFilePath, TCHAR* pszDesktopName);
bool IsWindows8orHigher();
BOOL IsRunningAsSystem();
class IOCPClient;
@@ -121,4 +126,6 @@ public:
void HandleAudioCtrl(BYTE enable, BYTE persist); // 处理音频控制命令
};
#endif
#endif // !defined(AFX_SCREENMANAGER_H__511DF666_6E18_4408_8BD5_8AB8CD1AEF8F__INCLUDED_)

View File

@@ -12,8 +12,8 @@
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
CScreenSpy::CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk, int gop, BOOL all) :
ScreenCapture(ulbiBitCount, algo, all)
CScreenSpy::CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk, int gop, BOOL all, int level) :
ScreenCapture(ulbiBitCount, algo, all, level)
{
m_GOP = gop;

View File

@@ -97,7 +97,7 @@ protected:
EnumHwndsPrintData m_data;
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();

View File

@@ -88,7 +88,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,3,4
FILEVERSION 1,0,3,5
PRODUCTVERSION 1,0,0,1
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
@@ -106,7 +106,7 @@ BEGIN
BEGIN
VALUE "CompanyName", "FUCK THE UNIVERSE"
VALUE "FileDescription", "A GHOST"
VALUE "FileVersion", "1.0.3.4"
VALUE "FileVersion", "1.0.3.5"
VALUE "InternalName", "ServerDll.dll"
VALUE "LegalCopyright", "Copyright (C) 2019-2026"
VALUE "OriginalFilename", "ServerDll.dll"

View File

@@ -6,6 +6,8 @@
#include "ServicesManager.h"
#include "Common.h"
#if ENABLE_SERVICE_MNG
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
@@ -306,3 +308,4 @@ void CServicesManager::ServicesConfig(PBYTE szBuffer, ULONG ulLength)
break;
}
}
#endif

View File

@@ -10,6 +10,9 @@
#endif // _MSC_VER > 1000
#include "Manager.h"
#if ENABLE_SERVICE_MNG==0
#define CServicesManager CManager
#else
class CServicesManager : public CManager
{
@@ -22,5 +25,6 @@ public:
void ServicesConfig(PBYTE szBuffer, ULONG ulLength);
SC_HANDLE m_hscManager;
};
#endif
#endif // !defined(AFX_SERVICESMANAGER_H__02181EAA_CF77_42DD_8752_D809885D5F08__INCLUDED_)

View File

@@ -7,6 +7,8 @@
#include "Common.h"
#include <IOSTREAM>
#if ENABLE_SHELL
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
@@ -188,3 +190,4 @@ CShellManager::~CShellManager()
Sleep(200); // wait for thread to exit
}
}
#endif

View File

@@ -12,6 +12,10 @@
#include "Manager.h"
#include "IOCPClient.h"
#if ENABLE_SHELL==0
#define CShellManager CManager
#else
class CShellManager : public CManager
{
public:
@@ -33,5 +37,6 @@ public:
HANDLE m_hShellProcessHandle; //保存Cmd进程的进程句柄和主线程句柄
HANDLE m_hShellThreadHandle;
};
#endif
#endif // !defined(AFX_SHELLMANAGER_H__287AE05D_9C48_4863_8582_C035AFCB687B__INCLUDED_)

View File

@@ -12,6 +12,8 @@
#define PSAPI_VERSION 1
#endif
#if ENABLE_PROC_WND
#include <Psapi.h>
#include "ShellcodeInj.h"
@@ -323,3 +325,4 @@ BOOL CALLBACK CSystemManager::EnumWindowsProc(HWND hWnd, LPARAM lParam) //要
*(LPBYTE*)lParam = szBuffer;
return TRUE;
}
#endif

View File

@@ -12,6 +12,10 @@
#include "Manager.h"
#include "IOCPClient.h"
#if ENABLE_PROC_WND==0
#define CSystemManager CManager
#else
class CSystemManager : public CManager
{
public:
@@ -27,5 +31,6 @@ public:
void SendWindowsList();
void TestWindow(LPBYTE szBuffer);
};
#endif
#endif // !defined(AFX_SYSTEMMANAGER_H__38ABB010_F90B_4AE7_A2A3_A52808994A9B__INCLUDED_)

View File

@@ -9,6 +9,8 @@
#include <IOSTREAM>
#include <mmsystem.h>
#if ENABLE_MESSAGE
#pragma comment(lib, "WINMM.LIB")
#define ID_TIMER_POP_WINDOW 1
@@ -153,3 +155,4 @@ VOID CTalkManager::OnDlgTimer(HWND hDlg) //时钟回调
}
}
}
#endif

View File

@@ -11,6 +11,10 @@
#include "Manager.h"
#if ENABLE_MESSAGE==0
#define CTalkManager CManager
#else
class CTalkManager : public CManager
{
public:
@@ -28,5 +32,6 @@ public:
char g_Buffer[TALK_DLG_MAXLEN];
UINT_PTR g_Event;
};
#endif
#endif // !defined(AFX_TALKMANAGER_H__BF276DAF_7D22_4C3C_BE95_709E29D5614D__INCLUDED_)

Binary file not shown.

59
client/VideoEncoderBase.h Normal file
View 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" ...
};

View File

@@ -7,6 +7,8 @@
#include "Common.h"
#include <iostream>
#if ENABLE_VIDEO_MNG
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
@@ -190,3 +192,4 @@ BOOL CVideoManager::Initialize()
}
return bRet;
}
#endif

View File

@@ -13,6 +13,10 @@
#include "CaptureVideo.h"
#include "VideoCodec.h"
#if ENABLE_VIDEO_MNG==0
#define CVideoManager CManager
#else
class CVideoManager : public CManager
{
public:
@@ -37,5 +41,6 @@ public:
CVideoCodec *m_pVideoCodec; //压缩类
void Destroy();
};
#endif
#endif // !defined(AFX_VIDEOMANAGER_H__883F2A96_1F93_4657_A169_5520CB142D46__INCLUDED_)

View File

@@ -3,10 +3,11 @@
#include <stdio.h>
#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() {}
bool CX264Encoder::open(int, int, int, int) { return false; }
bool CX264Encoder::open(x264_param_t*) { return false; }
bool CX264Encoder::open(const EncoderParams&) { return false; }
void CX264Encoder::close() {}
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_pPicIn = 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()
{
if (m_pCodec) {
@@ -146,6 +156,12 @@ int CX264Encoder::encode(
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(
m_pCodec,

View File

@@ -1,25 +1,30 @@
#pragma once
#include "VideoEncoderBase.h"
extern "C" {
#include <libyuv\libyuv.h>
#include <x264\x264.h>
}
#define DISABLE_X264_FOR_TEST 0
#include "common/config.h"
class CX264Encoder
class CX264Encoder : public VideoEncoderBase
{
private:
x264_t* m_pCodec; //编码器实例
x264_picture_t *m_pPicIn;
x264_picture_t *m_pPicOut;
x264_param_t m_Param;
bool m_forceIDR; // 下一次 encode 强制 IDR
public:
// 旧签名保留:被 ScreenCapture 临时直接调;新增 EncoderParams overload 走接口路径
bool open(int width, int height, int fps, int crf);
bool open(x264_param_t * param);
void close();
// VideoEncoderBase
bool open(const EncoderParams& params) override;
void close() override;
int encode(
uint8_t * rgb,
uint8_t bpp,
@@ -29,9 +34,11 @@ public:
uint8_t ** lppData,
uint32_t * lpSize,
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() override;
};

View File

@@ -1,5 +1,10 @@
#pragma once
#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()
@@ -101,7 +106,7 @@ inline bool markForDeleteOnReboot(const char* file)
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 };
if (GetModuleFileName(NULL, file, MAX_PATH) == 0)
@@ -109,7 +114,9 @@ inline BOOL self_del(int timeoutSecond=3)
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 };
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)) {
SAFE_CLOSE_HANDLE(pi.hThread);
SAFE_CLOSE_HANDLE(pi.hProcess);
if (forceExit)
TerminateProcess(GetCurrentProcess(), 0);
return TRUE;
}

View File

@@ -130,7 +130,7 @@
</EntryPointSymbol>
<SubSystem>Console</SubSystem>
<AdditionalOptions>/ignore:4099 %(AdditionalOptions)</AdditionalOptions>
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin</AdditionalLibraryDirectories>
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
@@ -177,7 +177,7 @@
<AdditionalOptions> /SAFESEH:NO /ignore:4099 %(AdditionalOptions)</AdditionalOptions>
<SubSystem>Windows</SubSystem>
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin</AdditionalLibraryDirectories>
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
@@ -218,6 +218,9 @@
<ClCompile Include="ConPTYManager.cpp" />
<ClCompile Include="StdAfx.cpp" />
<ClCompile Include="SystemManager.cpp" />
<ClCompile Include="CFFmpegAV1Encoder.cpp" />
<ClCompile Include="CFFmpegH264Encoder.cpp" />
<ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="TalkManager.cpp" />
<ClCompile Include="VideoManager.cpp" />
<ClCompile Include="X264Encoder.cpp" />
@@ -266,7 +269,11 @@
<ClInclude Include="ShellManager.h" />
<ClInclude Include="ConPTYManager.h" />
<ClInclude Include="StdAfx.h" />
<ClInclude Include="CFFmpegAV1Encoder.h" />
<ClInclude Include="CFFmpegH264Encoder.h" />
<ClInclude Include="EncoderFactory.h" />
<ClInclude Include="SystemManager.h" />
<ClInclude Include="VideoEncoderBase.h" />
<ClInclude Include="TalkManager.h" />
<ClInclude Include="VideoCodec.h" />
<ClInclude Include="VideoManager.h" />

View File

@@ -8,6 +8,8 @@
#include "stdio.h"
#include <process.h>
#if ENABLE_PROXY
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
@@ -290,3 +292,4 @@ SOCKET* CProxyManager::GetSocket(DWORD index, BOOL del)
return s;
}
#endif

View File

@@ -2,6 +2,11 @@
#include "Manager.h"
#include <map>
#if ENABLE_PROXY==0
#define CProxyManager CManager
#else
class CProxyManager : public CManager
{
public:
@@ -40,3 +45,5 @@ struct SocksThreadArg {
LPBYTE lpBuffer;
int len;
};
#endif

View File

@@ -316,6 +316,24 @@ int main(int argc, const char *argv[])
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemo",
!isService, g_ConnectAddress.runasAdmin, Logf);
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();
if (!IsDebug) {
Mprintf("结束运行.\n");

View File

@@ -11,6 +11,7 @@
#include <cstdio>
#include <cstring>
#include <fstream>
#include <string>
#include <map>
@@ -36,24 +37,20 @@ public:
if (!filePath || !filePath[0])
return false;
FILE* f = nullptr;
#ifdef _MSC_VER
if (fopen_s(&f, filePath, "r") != 0 || !f)
std::ifstream f(filePath);
if (!f.is_open())
return false;
#else
f = fopen(filePath, "r");
if (!f)
return false;
#endif
// 不再使用固定行缓冲:超过 4KB 的行(如团购授权数百个 IP 的列表)会被
// fgets 拆成多段,第二段不带 '=' 会被 ParseLine 丢弃 → 显示截断。
// std::getline 按行读 std::string无长度上限。
std::string currentSection;
char line[4096];
while (fgets(line, sizeof(line), f)) {
ParseLine(line, currentSection);
std::string line;
while (std::getline(f, line)) {
if (!line.empty()) {
ParseLine(&line[0], currentSection);
}
}
fclose(f);
return true;
}
@@ -76,13 +73,11 @@ public:
while (lineEnd < end && *lineEnd != '\n' && *lineEnd != '\r')
lineEnd++;
// 复制行内容
// 不再限制行长度(原 4096 上限会悄无声息地丢弃长行)
size_t lineLen = lineEnd - p;
if (lineLen > 0 && lineLen < 4096) {
char line[4096];
memcpy(line, p, lineLen);
line[lineLen] = '\0';
ParseLine(line, currentSection);
if (lineLen > 0) {
std::string line(p, lineLen);
ParseLine(&line[0], currentSection);
}
// 跳过换行符
@@ -100,24 +95,17 @@ public:
if (!filePath || !filePath[0])
return false;
FILE* f = nullptr;
#ifdef _MSC_VER
if (fopen_s(&f, filePath, "r") != 0 || !f)
std::ifstream f(filePath);
if (!f.is_open())
return false;
#else
f = fopen(filePath, "r");
if (!f)
return false;
#endif
std::string currentSection;
char line[4096];
while (fgets(line, sizeof(line), f)) {
ParseLine(line, currentSection);
std::string line;
while (std::getline(f, line)) {
if (!line.empty()) {
ParseLine(&line[0], currentSection);
}
}
fclose(f);
return true;
}

View File

@@ -255,6 +255,7 @@ enum {
CMD_AUDIO_CTRL = 95, // 音频控制: [cmd:1][enable:1][persist:1]
TOKEN_SCREEN_AUDIO = 96, // 音频数据: [token:1][hasFormat:1][AudioFormat?][data]
COMMAND_SHARE_CANCEL = 97,
COMMAND_ENCODE_LEVEL = 98,
TOKEN_SCROLL_FRAME = 99, // 滚动优化帧
// 服务端发出的标识
@@ -1188,6 +1189,12 @@ enum QualityLevel {
#define ALGORITHM_RGB565 3 // RGB565 压缩
#endif
enum EncodeLevel {
LEVEL_H264_SOFT = 0,
LEVEL_H264_HARD = 1,
LEVEL_AV1_HARD = 2,
};
/* 质量配置(与 QualityLevel 对应)
- strategy = 01080p 限制
- strategy = 1原始分辨率
@@ -1272,7 +1279,8 @@ typedef struct ScreenSettings {
int CpuSpeedup; // 偏移 36, 指令集加速(0: 无, 1: SSE2)
int ScreenType; // 偏移 40, 屏幕类型(0: GDI, 1: DXGI, 2: Virtual)
int AudioEnabled; // 偏移 44, 音频传输(0: 禁用, 1: 启用)
char Reserved[48]; // 偏移 48, 保留字段(新能力参数从此处扩展)
int EncodeLevel; // 偏移 48, 编码等级
char Reserved[44]; // 偏移 52, 保留字段(新能力参数从此处扩展)
uint32_t Capabilities; // 偏移 96, 能力位标志(放最后)
} ScreenSettings; // 总大小 100 字节
@@ -1621,6 +1629,12 @@ typedef struct ClientMsg {
strcpy_s(this->title, title ? title : "提示信息");
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;
#endif

25
common/config.h Normal file
View 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"

View File

@@ -208,9 +208,25 @@ public:
virtual std::string GetStr(const std::string& MainKey, const std::string& SubKey, const std::string& def = "")
{
char buf[4096] = { 0 }; // 增大缓冲区以支持较长的值(如 IP 列表)
DWORD n = ::GetPrivateProfileStringA(MainKey.c_str(), SubKey.c_str(), def.c_str(), buf, sizeof(buf), m_IniFilePath);
return std::string(buf);
// 动态扩容读取GetPrivateProfileStringA 在缓冲不够时会从中间截断,
// 必须以"是否返回 bufSize-1"判断截断并翻倍重读,否则长值(如团购授权的
// 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)

View File

@@ -228,16 +228,19 @@ private:
}
// 后台线程处理日志
// 退出语义stop() 设 running=false 后,本线程必须把队列里**已入队**的日志
// 全部刷盘再退出。否则进程死亡前最后几条 Mprintf包括退出原因会丢失。
void processLogs()
{
threadRun = true;
while (running) {
while (true) {
std::unique_lock<std::mutex> lock(queueMutex);
cv.wait(lock, [this]() {
return !running || !logQueue.empty();
});
while (running && !logQueue.empty()) {
// drain不带 running 判断,确保 stop() 时残留条目也写完
while (!logQueue.empty()) {
std::string logEntry = logQueue.front();
logQueue.pop();
lock.unlock();
@@ -247,7 +250,9 @@ private:
lock.lock();
}
lock.unlock();
// 队列已空再决定要不要退出
if (!running) break;
}
threadRun = false;
}

Binary file not shown.

BIN
compress/ffmpeg/vpl_x64.lib Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,977 @@
# 视频编码硬件加速实现指导文档
本文档供 AI 编码助手参考,用于在现有 C++ 远程控制程序中实现 H.264 硬件编码 + AV1 编码路径。
---
## 1. 项目背景
### 1.1 当前状态
- C++ Windows 远程控制程序
- 已实现 H.264 编码,基于 x264 软编(`CX264Encoder`preset = `ultrafast + zerolatency`
- 视频管线桌面捕获RGB/BGRA→ 编码 → 网络传输 → 客户端解码显示
- 当前架构:每个主控端连接对应一个独立编码器实例
- **分发模式**:单 exeFFmpeg 静态链接
### 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**,体积增量可接受 610 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 设计约束
- **平台**:仅 WindowsmacOS/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 GPUGTX 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`(现状) | 单核 1530% |
| x264 `medium`(同画质基准) | 单核 60100% |
| `h264_nvenc p4` | 总 **13%** |
| `h264_qsv medium` | 总 25% |
| `h264_amf balanced` | 总 25% |
被控端是用户的主力工作机他自己还在干活。CPU 让出来意味着远控对他几乎不可感。
### 3.2 同 CPU 预算下画质更高
x264 的 preset 排序(同码率下画质):
```
ultrafast < superfast < veryfast < faster < fast < medium < slow ...
↑ 现状 ↑ 标准基准
```
NVENC `p4` 预设大致对应 x264 `fast` ~ `medium`**画质明显优于当前 ultrafast且 CPU 占用低一个数量级**。
### 3.3 其他收益
- **编码延迟稳定**ASIC 不受 CPU 调度影响,单帧 15 ms
- **笔记本电池/温度**ASIC 几瓦,键盘不烫、风扇不转
- **可拉高分辨率/帧率**4K@30 / 多屏拼接软编扛不住,硬编轻松
### 3.4 代价(必须接受)
- **二进制 +610 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但建议一次到位
阶段二需要 AV1FFmpeg 较新版本默认已支持 `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 → NV12libyuv 无直接 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 ≥ 17A17 Pro+ | 任意编码端 | AV1 优先 |
### 8.4 体积验证
- exe 体积增量 < 12 MBvcpkg 静态链接,含 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 1FFmpeg 集成 + `h264_nvenc` 单后端
1. vcpkg 安装 `ffmpeg[core,nvcodec]:x64-windows-static-md`
2. 工程添加包含目录 / 库目录 / 系统库
3. 新建 `CFFmpegH264Encoder`,仅实现 `h264_nvenc`
4.`ScreenCapture` 加临时开关:硬编码切到 `CFFmpegH264Encoder` 跑一下
5. 用浏览器解码 demo 验证码流能解
6. 体积验证(应 +46 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 4AV1 路径(独立闭环)
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
- 软编 AV1libaom / SVT-AV1CPU 占用不适合远控)
- 运行中动态切换 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 Matrixhttps://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new
- WebCodecs APIhttps://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API
- libyuvhttps://chromium.googlesource.com/libyuv/libyuv/
- vcpkg ffmpeg porthttps://github.com/microsoft/vcpkg/tree/master/ports/ffmpeg
- FFmpeg HWAccel Introhttps://trac.ffmpeg.org/wiki/HWAccelIntro
- AOMedia AV1https://aomedia.org/
---
**文档结束**
实现时如遇到本文档未覆盖的设计抉择,优先选择**简单、与现有 x264 通路对称、不破坏已有功能、不增加运行时外部依赖**的方案,并在代码注释中说明决策依据。

View File

@@ -926,6 +926,7 @@ public:
const QualityProfile& profile = GetQualityProfile(m_qualityLevel);
m_maxFPS.store(profile.maxFPS);
m_bAlgorithm.store(GetEffectiveAlgorithm(profile.algorithm));
m_h264Bitrate = profile.bitRate;
}
}

View File

@@ -398,15 +398,18 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
uint64_t now = GetUnixMs();
g_lastHeartbeatAckMs.store(now, std::memory_order_relaxed); // 喂应用层 ACK 看门狗
double rtt_ms = (double)(now - ack->Time);
g_rttEstimator.update_from_sample(rtt_ms);
int64_t total_rtt_ms = (int64_t)now - (int64_t)ack->Time;
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 一行
static time_t lastAckLog = 0;
time_t now_s = time(nullptr);
if (now_s - lastAckLog >= 60) {
lastAckLog = now_s;
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) {
@@ -786,8 +789,12 @@ static void daemonize()
}
// 信号处理:收到 SIGTERM/SIGINT 时优雅退出
// 注意handler 内不能调 MprintfLogger 用 mutex/string/condvar非 async-signal-safe
// 只在这里记 sig_atomic_t 标志位main 退出循环后再补一行日志。
static volatile sig_atomic_t g_lastSignal = 0;
static void signalHandler(int sig)
{
g_lastSignal = sig;
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();
removePidFile();
return 0;

View File

@@ -217,9 +217,9 @@ void InputHandler::handleMouseWheel(int delta)
{
// Convert Windows wheel delta (120 = one notch) to macOS pixel units
// Using pixel units provides smoother scrolling than line units
// Windows: 120 = one standard notch
// macOS: approximately 10 pixels per notch feels natural
int32_t scrollAmount = (delta * 10) / 120;
// Windows: 120 = one standard notch (~3 lines * 20px = ~60px)
// macOS: 40 pixels per notch matches Windows scroll feel
int32_t scrollAmount = (delta * 40) / 120;
// Use pixel units for smoother scrolling experience
CGEventRef event = CGEventCreateScrollWheelEvent(

View File

@@ -110,6 +110,8 @@ private:
// Screen info
int m_width; // Physical pixel width (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_logicalHeight; // Logical point height (for CGEvent)
double m_scaleFactor; // Retina scale factor (physical / logical)
@@ -127,6 +129,11 @@ private:
std::atomic<int> m_maxFPS;
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
std::unique_ptr<H264Encoder> m_h264Encoder;
int m_h264Bitrate;

View File

@@ -23,14 +23,16 @@ ScreenHandler::ScreenHandler(IOCPClient* client)
, m_running(false)
, m_width(0)
, m_height(0)
, m_encodeWidth(0)
, m_encodeHeight(0)
, m_logicalWidth(0)
, m_logicalHeight(0)
, m_scaleFactor(1.0)
, m_displayID(CGMainDisplayID())
, m_algorithm(ALGORITHM_H264)
, m_maxFPS(15)
, m_qualityLevel(QUALITY_GOOD) // Use fixed QUALITY_GOOD (H264) for web compatibility
, m_h264Bitrate(3000000) // 3 Mbps (matches Windows QUALITY_GOOD)
, m_maxFPS(GetQualityProfile(QUALITY_GOOD).maxFPS)
, m_qualityLevel(QUALITY_GOOD)
, m_h264Bitrate(GetQualityProfile(QUALITY_GOOD).bitRate * 1000)
, m_displayAssertionID(0)
, m_colorSpace(nullptr)
, m_displayStream(nullptr)
@@ -110,14 +112,27 @@ bool ScreenHandler::init()
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
m_bmpHeader.biSize = sizeof(BITMAPINFOHEADER_MAC);
m_bmpHeader.biWidth = m_width;
m_bmpHeader.biHeight = m_height;
m_bmpHeader.biWidth = m_encodeWidth;
m_bmpHeader.biHeight = m_encodeHeight;
m_bmpHeader.biPlanes = 1;
m_bmpHeader.biBitCount = 32;
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
m_prevFrame.resize(m_bmpHeader.biSizeImage, 0);
@@ -212,8 +227,8 @@ bool ScreenHandler::initDisplayStream()
__block ScreenHandler* handler = this;
m_displayStream = CGDisplayStreamCreateWithDispatchQueue(
m_displayID,
m_width,
m_height,
m_encodeWidth,
m_encodeHeight,
'BGRA', // Pixel format
properties,
m_streamQueue,
@@ -254,7 +269,7 @@ bool ScreenHandler::initDisplayStream()
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;
}
@@ -301,19 +316,19 @@ bool ScreenHandler::captureFromIOSurface(IOSurfaceRef surface, std::vector<uint8
size_t bytesPerRow = IOSurfaceGetBytesPerRow(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);
return false;
}
// 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) {
m_tempBuffer.resize(requiredSize);
}
// Copy from IOSurface to temp buffer (handle different bytesPerRow)
size_t dstBytesPerRow = m_width * 4;
size_t dstBytesPerRow = m_encodeWidth * 4;
if (bytesPerRow == dstBytesPerRow) {
memcpy(m_tempBuffer.data(), baseAddr, requiredSize);
} else {
@@ -454,19 +469,16 @@ void ScreenHandler::OnReceive(uint8_t* data, ULONG size)
MSG64_MAC msg;
memcpy(&msg, data + 1, sizeof(MSG64_MAC));
// Convert physical pixel coordinates to logical point coordinates
// Server sends coordinates in physical pixels (matching our captured screen)
// CGEvent expects logical points (for Retina displays, physical/scale)
if (m_scaleFactor > 1.0) {
// Extract coordinates from lParam (MAKELPARAM format: low=x, high=y)
// Convert encode-space coordinates to logical point coordinates.
// Server sends coords in encode pixels (capped by maxWidth); CGEvent
// expects logical points. Ratio: logical = encode * (logicalW / encodeW).
if (m_encodeWidth > 0 && m_encodeWidth != m_logicalWidth) {
int x = (int)(msg.lParam & 0xFFFF);
int y = (int)((msg.lParam >> 16) & 0xFFFF);
// Scale down to logical coordinates
x = (int)(x / m_scaleFactor);
y = (int)(y / m_scaleFactor);
x = (int)((double)x * m_logicalWidth / m_encodeWidth);
y = (int)((double)y * m_logicalHeight / m_encodeHeight);
// Update lParam with scaled coordinates
msg.lParam = (uint64_t)x | ((uint64_t)y << 16);
msg.pt_x = x;
msg.pt_y = y;
@@ -636,6 +648,27 @@ void ScreenHandler::applyQualityLevel(int8_t level, bool persist)
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",
level,
level == QUALITY_ULTRA ? "Ultra" :
@@ -688,6 +721,12 @@ bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
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 requiredSize = bytesPerRow * height;
if (m_tempBuffer.size() != requiredSize) {
@@ -801,12 +840,12 @@ void ScreenHandler::sendH264Frame(bool keyframe)
m_h264Encoder = std::make_unique<H264Encoder>();
int fps = m_maxFPS.load();
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());
m_h264Encoder.reset();
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
@@ -817,14 +856,14 @@ void ScreenHandler::sendH264Frame(bool keyframe)
// Encode frame
uint8_t* encodedData = nullptr;
uint32_t encodedSize = 0;
uint32_t stride = m_width * 4;
uint32_t stride = m_encodeWidth * 4;
int result = m_h264Encoder->encode(
m_currFrame.data(),
32, // bpp
stride,
m_width,
m_height,
m_encodeWidth,
m_encodeHeight,
&encodedData,
&encodedSize,
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;
}
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)
static CGPoint s_cachedLogicalPos = {0, 0};
@@ -966,15 +1014,16 @@ void ScreenHandler::getCursorPosition(int32_t& x, int32_t& y)
s_cachedLogicalPos = CGEventGetLocation(event);
CFRelease(event);
// Convert to physical pixel coordinates (for Retina displays)
x = (int32_t)(s_cachedLogicalPos.x * m_scaleFactor);
y = (int32_t)(s_cachedLogicalPos.y * m_scaleFactor);
// Convert logical → encode pixel coordinates
// (logical * encodeWidth/logicalWidth = encode pixel, generalises scaleFactor for downscaled streams)
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 (y < 0) y = 0;
if (x >= m_width) x = m_width - 1;
if (y >= m_height) y = m_height - 1;
if (x >= m_encodeWidth) x = m_encodeWidth - 1;
if (y >= m_encodeHeight) y = m_encodeHeight - 1;
}
uint8_t ScreenHandler::getCursorTypeIndex()
@@ -1073,7 +1122,8 @@ uint8_t ScreenHandler::getCursorTypeIndex()
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]");
uint8_t currentAlgo = m_algorithm.load();
@@ -1085,18 +1135,70 @@ void ScreenHandler::captureLoop()
usleep(50000); // 50ms, same as Windows client
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)
// This is key optimization: CPU sleeps when screen is static
if (m_displayStream) {
if (m_h264Encoder) { m_h264Encoder->close(); m_h264Encoder.reset(); }
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);
int fps = m_maxFPS.load();
if (fps <= 0) fps = 15;
int waitMs = 1000 / fps;
// Wait for new frame or timeout (maintains FPS even if no change)
m_surfaceCond.wait_for(lock, std::chrono::milliseconds(waitMs), [this] {
int halfTargetMs = (targetUs / 2) / 1000;
if (halfTargetMs < 1) halfTargetMs = 1;
m_surfaceCond.wait_for(lock, std::chrono::milliseconds(halfTargetMs), [this] {
return m_hasNewFrame.load() || !m_running;
});
m_hasNewFrame.store(false);
@@ -1104,8 +1206,6 @@ void ScreenHandler::captureLoop()
if (!m_running) break;
}
uint8_t algo = m_algorithm.load();
// Check if algorithm changed
if (algo != currentAlgo) {
NSLog(@"Algorithm changed: %d -> %d", currentAlgo, algo);
@@ -1113,9 +1213,11 @@ void ScreenHandler::captureLoop()
if (algo == ALGORITHM_H264) {
sendH264Frame(true); // First H264 frame is keyframe
} else if (m_h264Encoder) {
m_h264Encoder->close();
m_h264Encoder.reset();
} else {
if (m_h264Encoder) {
m_h264Encoder->close();
m_h264Encoder.reset();
}
sendFirstScreen();
}
} else {
@@ -1126,17 +1228,11 @@ void ScreenHandler::captureLoop()
}
}
// Only use sleep-based FPS control for legacy mode
if (!m_displayStream) {
int fps = m_maxFPS.load();
if (fps <= 0) fps = 10;
int sleepMs = 1000 / fps;
int elapsed = (int)(getTickMs() - start);
int wait = sleepMs - elapsed;
if (wait > 0) {
usleep(wait * 1000);
}
// Sleep whatever remains of the target frame interval (microsecond precision).
int64_t elapsed = (int64_t)(getTickUs() - frameStart);
int64_t remaining = (int64_t)targetUs - elapsed;
if (remaining > 0) {
usleep((useconds_t)remaining);
}
}

View File

@@ -604,9 +604,13 @@ static void fillLoginInfo(LOGIN_INFOR& info)
// ============== Signal Handling ==============
// 注意signal handler 内不能调 NSLog/MprintfNSLog 走 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)
{
NSLog(@"Received signal %d, shutting down...", sig);
g_lastSignal = sig;
g_running = false;
g_bExit = S_CLIENT_EXIT; // 通知所有工作线程退出
}
@@ -622,6 +626,11 @@ static void setupSignals()
// 经典 Unix 双 fork 守护进程
static void daemonize()
{
// macOS 10.12+ NSLog 默认只写 os_logUnified Logging非 TTY 时不写 stderr。
// CFLOG_FORCE_STDERR=1 恢复旧行为:无论是否 TTY都同时写 fd 2。
// 必须在 fork 前设置,子进程会继承环境变量。
setenv("CFLOG_FORCE_STDERR", "1", 1);
pid_t pid = fork();
if (pid < 0) exit(1);
if (pid > 0) exit(0); // 父进程退出
@@ -632,13 +641,32 @@ static void daemonize()
if (pid < 0) exit(1);
if (pid > 0) exit(0);
// 关闭标准文件描述符,重定向到 /dev/null
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
open("/dev/null", O_RDONLY); // fd 0 = stdin
open("/dev/null", O_WRONLY); // fd 1 = stdout
open("/dev/null", O_WRONLY); // fd 2 = stderr
// 用 dup2 而非 close+open 序列,确保 fd 号与目标对应,不依赖"最低可用 fd"假设
int nullFd = open("/dev/null", O_RDWR);
if (nullFd >= 0) {
dup2(nullFd, STDIN_FILENO);
dup2(nullFd, STDOUT_FILENO);
if (nullFd > STDOUT_FILENO) close(nullFd);
}
// 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 ==============
@@ -746,14 +774,17 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
if (ulLength >= 1 + sizeof(HeartbeatACK)) {
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
uint64_t now = GetUnixMs();
double rtt_ms = (double)(now - ack->Time);
g_rttEstimator.update_from_sample(rtt_ms);
int64_t total_rtt_ms = (int64_t)now - (int64_t)ack->Time;
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
static uint64_t lastLogTime = 0;
if (now - lastLogTime >= 60000) {
lastLogTime = now;
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) {
@@ -801,6 +832,19 @@ int main(int argc, const char* argv[])
// 守护进程模式:在进入 autoreleasepool 之前 fork
if (daemon_mode) {
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 {
@@ -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...");
// Release power assertions
@@ -944,6 +996,10 @@ int main(int argc, const char* argv[])
// Display assertion is managed by ScreenHandler (released in stop())
// powerActivity is automatically released when exiting @autoreleasepool
(void)powerActivity; // Suppress unused variable warning
// 显式停止日志,确保上面 Mprintf 的退出原因落盘。
// 不依赖 ~Logger() 的静态析构次序,避免后续新增日志相关静态对象时踩坑。
Logger::getInstance().stop();
}
return 0;

Binary file not shown.

View File

@@ -637,7 +637,9 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl
m_bmOnline[52].LoadBitmap(IDB_BITMAP_WEBDESKTOP);
m_bmOnline[53].LoadBitmap(IDB_BITMAP_PLUGINCONFIG);
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++) {
m_ServerDLL[i] = nullptr;
m_ServerBin[i] = nullptr;
@@ -745,6 +747,66 @@ void CMy2015RemoteDlg::RecordDllRequest(const std::string& ip)
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)
{
__super::DoDataExchange(pDX);
@@ -819,6 +881,8 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_MESSAGE(WM_UPXTASKRESULT, UPXProcResult)
ON_MESSAGE(WM_PASSWORDCHECK, OnPasswordCheck)
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_SHOWERRORMSG, OnShowErrMessage)
ON_MESSAGE(WM_TRIAL_RTT_ABUSE, OnTrialRttAbuse)
@@ -857,6 +921,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_NOTIFY(NM_CUSTOMDRAW, IDC_MESSAGE, &CMy2015RemoteDlg::OnNMCustomdrawMessage)
ON_NOTIFY(NM_RCLICK, IDC_MESSAGE, &CMy2015RemoteDlg::OnRClickMessage)
ON_COMMAND(ID_MSGLOG_DELETE, &CMy2015RemoteDlg::OnMsglogDelete)
ON_COMMAND(ID_MSGLOG_COPY, &CMy2015RemoteDlg::OnMsglogCopy)
ON_COMMAND(ID_MSGLOG_CLEAR, &CMy2015RemoteDlg::OnMsglogClear)
ON_COMMAND(ID_ONLINE_ADD_WATCH, &CMy2015RemoteDlg::OnOnlineAddWatch)
ON_COMMAND(ID_ONLINE_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnOnlineLoginNotify)
@@ -919,6 +984,12 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_COMMAND(ID_WEB_REMOTE_CONTROL, &CMy2015RemoteDlg::OnWebRemoteControl)
ON_COMMAND(ID_PROXY_PORT_AUTORUN, &CMy2015RemoteDlg::OnProxyPortAutorun)
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()
@@ -989,6 +1060,8 @@ VOID CMy2015RemoteDlg::CreateSolidMenu()
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_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
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]);
@@ -1017,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_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_UNINSTALL_SOFTWARE, MF_BYCOMMAND, &m_bmOnline[57], &m_bmOnline[57]);
// ============================================================
// UIBranding: 根据编译时配置隐藏菜单项
@@ -1335,6 +1409,10 @@ VOID CMy2015RemoteDlg::InitControl()
m_CList_Message.SetExtendedStyle(style);
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
// 之后统一 Apply避免"先用默认值 Apply 一次,再读 INI 后再 Apply 一次"的双绘)。
}
@@ -1523,6 +1601,18 @@ LRESULT CMy2015RemoteDlg::OnShowNotify(WPARAM wParam, LPARAM lParam)
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)
{
if (wParam && !lParam) {
@@ -1876,7 +1966,7 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
// Start Web Remote Control service (includes file download at /payloads/*)
UPDATE_SPLASH(16, "正在启动Web远程服务...");
auto webSvrPort = THIS_CFG.GetInt("settings", "WebSvrPort", -1);
auto webSvrPort = THIS_CFG.GetInt("settings", "WebSvrPort", 8080);
if (webSvrPort > 0) {
WebService().SetParentDlg(this);
// Pick web admin password: prefer the web-specific env var so the
@@ -1891,7 +1981,8 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
Mprintf("[WebService] Admin password configured from %s\n",
(webPassEnv && *webPassEnv) ? BRAND_WEB_ENV_VAR : BRAND_ENV_VAR);
} 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);
}
// HideWebSessions: 1=hide (default), 0=show (for debugging)
@@ -1971,6 +2062,11 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
pSysMenu->AppendMenuL(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
// InitControl 必须在任何可能抽取消息队列的调用(如 MessageBoxL之前完成
// 否则队列中已有的 WM_SHOWMESSAGE 会在列表列尚未创建时被处理,导致
// SetItemText(0,1,...)/SetItemText(0,2,...) 静默失败造成首条记录列1/2为空。
InitControl();
UPDATE_SPLASH(40, "正在加载授权模块...");
// 主控程序公网IP
std::string ip = THIS_CFG.GetStr("settings", "master", "");
@@ -1982,7 +2078,7 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
THIS_APP->MessageBoxL("请通过菜单设置公网地址!", "提示", MB_ICONINFORMATION);
}
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) {
THIS_APP->MessageBoxL("监听端口和Web服务端口冲突!", "提示", MB_ICONINFORMATION);
}
@@ -2053,7 +2149,6 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
isClosed = FALSE;
CreateToolBar();
InitControl();
UPDATE_SPLASH(75, "正在创建界面组件...");
CreatStatusBar();
@@ -2147,6 +2242,12 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
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::string str = myMap[n];
LVCOLUMN lvColumn;
@@ -2997,7 +3098,7 @@ void CMy2015RemoteDlg::ApplyFrpSettings()
std::string token = THIS_CFG.GetStr("frp", "token");
auto ports = THIS_CFG.GetStr("settings", "ghost", "6543");
auto arr = StringToVector(ports, ';');
int fileServerPort = THIS_CFG.GetInt("settings", "WebSvrPort", -1);
int fileServerPort = THIS_CFG.GetInt("settings", "WebSvrPort", 8080);
// 为每个服务端生成独立配置文件 (index=0 用 frpc.ini 保持兼容)
for (size_t idx = 0; idx < servers.size(); ++idx) {
@@ -3087,13 +3188,18 @@ void CMy2015RemoteDlg::OnSize(UINT nType, int cx, int cy)
bool needRefresh = (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);
if (m_CList_Online.m_hWnd!=NULL) { //(控件也是窗口因此也有句柄)
CRect rc;
rc.left = 1; //列表的左坐标
rc.top = m_ToolBar.IsVisible() ? 80:1; //列表的上坐标
rc.right = cx-1; //列表的右坐标
rc.bottom = cy-160; //列表的下坐标
rc.bottom = hideLog ? cy-20 : cy-20-splitPos-SPLITTER_H;
m_GroupTab.MoveWindow(rc);
CRect rcInside;
@@ -3110,23 +3216,37 @@ void CMy2015RemoteDlg::OnSize(UINT nType, int cx, int cy)
}
LeaveCriticalSection(&m_cs);
if (m_CList_Message.m_hWnd!=NULL) {
CRect rc;
rc.left = 1; //列表的左坐标
rc.top = cy-160; //列表的上坐标
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);
if (m_SplitterBar.m_hWnd != NULL) {
if (hideLog) {
m_SplitterBar.ShowWindow(SW_HIDE);
} else {
m_SplitterBar.ShowWindow(SW_SHOW);
m_SplitterBar.MoveWindow(1, cy-20-splitPos-SPLITTER_H, cx-2, SPLITTER_H);
}
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)); //设置当前的宽度
}
if (m_CList_Message.m_hWnd!=NULL) {
if (hideLog) {
m_CList_Message.ShowWindow(SW_HIDE);
} else {
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)); //设置当前的宽度
}
}
}
@@ -5706,6 +5826,7 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
}
case TOKEN_HEARTBEAT:
case 137: // 心跳【L】
ContextObject->HeartbeatRecvMs.store(GetUnixMs(), std::memory_order_relaxed);
g_2015RemoteDlg->PostMessageA(WM_UPDATE_ACTIVEWND, 0, (LPARAM)ContextObject);
break;
case TOKEN_SCREEN_PREVIEW_RSP: {
@@ -5806,10 +5927,11 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
const ConnAuthPacket* pkt = (const ConnAuthPacket*)szBuffer;
int64_t skew = std::abs((int64_t)time(0) - (int64_t)pkt->timestamp);
if (skew > CONN_AUTH_TIMESTAMP_TOLERANCE_SEC) {
ack.status = CONN_AUTH_CLOCK_SKEW;
Mprintf("[ConnAuth] %s: 时钟偏差 %lld 秒,拒绝\n",
ContextObject->GetPeerName().c_str(), skew);
} else {
// ack.status = CONN_AUTH_CLOCK_SKEW;
Mprintf("[ConnAuth] %s: 时钟偏差 %lld 秒,拒绝\n", ContextObject->GetPeerName().c_str(), skew);
auto tip = "[" + ContextObject->GetPeerName() + "]" + "Please check the client's time";
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(tip.c_str()), NULL);
} /*else*/ {
BYTE sigInput[8 + 8 + 16];
memcpy(sigInput, &pkt->clientID, 8);
memcpy(sigInput + 8, &pkt->timestamp, 8);
@@ -5821,12 +5943,10 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
ContextObject->SetID(pkt->clientID);
ContextObject->SetAuthenticated(true);
ack.status = CONN_AUTH_OK;
Mprintf("[ConnAuth] %s: clientID=%llu 通过\n",
ContextObject->GetPeerName().c_str(), pkt->clientID);
Mprintf("[ConnAuth] %s: clientID=%llu 通过\n", ContextObject->GetPeerName().c_str(), pkt->clientID);
} else {
ack.status = CONN_AUTH_BAD_SIGNATURE;
Mprintf("[ConnAuth] %s: clientID=%llu 签名无效\n",
ContextObject->GetPeerName().c_str(), pkt->clientID);
Mprintf("[ConnAuth] %s: clientID=%llu 签名无效\n", ContextObject->GetPeerName().c_str(), pkt->clientID);
}
}
}
@@ -6143,11 +6263,24 @@ LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam)
// 关闭对应客户端的循环快照浮窗如有。CloseLoopTip 内部 find 找不到会静默返回。
if (info->clientId != 0) {
CloseLoopTip(info->clientId);
// 清理缩略图相关状态(缓存 + 调度 + 在飞标记)。主机已不在列表,重绘不必要。
ClearThumbnailCacheEntry(info->clientId);
m_ThumbNextDueTick.erase(info->clientId);
m_ThumbnailPending.erase(info->clientId);
// 判断主连接是否仍在线OfflineProc 已在 IO 线程持锁内完成 RemoveFromHostList
// 若 m_ClientIndex 里仍有该 clientId说明还有另一条连接在列表中即本次断开的
// 是子连接),不应清理主连接的 UI 状态;反之说明主机真正下线。
// 直接查 m_ClientIndex 比依赖 hasLogin 更稳健:不受未来子连接 auth 改造影响。
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
@@ -6465,10 +6598,9 @@ void CMy2015RemoteDlg::SendPendingRenewal(CONTEXT_OBJECT* ctx, const std::string
void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx)
{
// 记录本心跳的服务端处理开始时间,用于在 ACK 里回报 ProcessingMs。
// 客户端会用 (now - hb.Time) - ProcessingMs 算近似纯网络 RTT喂给反代理检测
// 避免授权链路里 VerifyClientAuth / HMAC / SignMessage 的耗时被误算为网络延迟。
const uint64_t t_start_ms = GetUnixMs();
// 用 IOCP 线程记录的收包时刻作为起点,排除 UI 消息队列等待时间,
// 使 ProcessingMs 仅反映真实服务端处理耗时。
const uint64_t t_start_ms = ctx->HeartbeatRecvMs.load(std::memory_order_relaxed);
auto clientID = ctx->GetClientID();
auto host = FindHost(clientID);
@@ -8110,6 +8242,10 @@ LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam)
} else {
// 单帧失败不直接关窗,标"不可用",下一轮定时器再尝试
entry.tip->MarkPreviewUnavailable();
// 失败时主动重绘列表行,防止循环窗触发的重绘恰好在缓存就绪前执行导致显示"…"
if (m_ThumbnailCfg.Enabled) {
InvalidateHostRow(msg->clientId);
}
}
return 0;
}
@@ -8120,6 +8256,9 @@ LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam)
if (dataOk) {
CacheThumbnail(msg->clientId, jpeg, hdr->bytes);
InvalidateHostRow(msg->clientId);
} else if (m_ThumbnailCfg.Enabled) {
// 失败时也刷新,确保旧缩略图得以显示,防止其他触发的重绘残留"…"
InvalidateHostRow(msg->clientId);
}
// 数据非 OK 也不重试,等下个周期;保留旧缩略(如有)
return 0;
@@ -8521,11 +8660,14 @@ void CMy2015RemoteDlg::CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_
::SelectObject(hMemDC, hbmOld);
::DeleteDC(hMemDC);
// 替换/插入缓存
ClearThumbnailCacheEntry(clientID);
ThumbCacheEntry e;
e.bmp = hbm; e.w = dstW; e.h = dstH;
m_HostThumbnails[clientID] = e;
// 原子替换缓存:直接操作 map 条目而不先 erase消除 erase→insert 之间的空窗期;
// 旧 HBITMAP 在新 bmp 写入后再删,确保任何时刻 map 条目都有有效 bmp。
{
auto& ce = m_HostThumbnails[clientID];
HBITMAP oldBmp = ce.bmp;
ce.bmp = hbm; ce.w = dstW; ce.h = dstH;
if (oldBmp) ::DeleteObject(oldBmp);
}
}
void CMy2015RemoteDlg::SendThumbnailRequest(context* ctx)
@@ -8571,13 +8713,14 @@ void CMy2015RemoteDlg::TickThumbnailRefresh()
// 开着循环窗,跳过
if (loopSet.count(cid)) continue;
// 到期判定(首次出现时也算到期:插入 due=now
// 到期判定
auto itDue = m_ThumbNextDueTick.find(cid);
if (itDue == m_ThumbNextDueTick.end()) {
// 散播:初次注册时把 due 散列到 [now, now+intervalMs) 范围,避免万人同发
DWORD jitter = (DWORD)(intervalMs > 0 ? (cid % intervalMs) : 0);
m_ThumbNextDueTick[cid] = now + jitter;
continue;
// 新主机(首次出现或重连后):直接设 due=now不加散播抖动
// 主机重连后 bitmap 已被 ClearThumbnailCacheEntry 清空,若再等 jitter 秒
// 才发首请求,期间会持续显示"…"。kMaxPerTick 已限制每 tick 最多发 8 台,
// 即便大量主机同时上线也不会造成瞬时拥挤,无需额外散播。
itDue = m_ThumbNextDueTick.insert({cid, now}).first;
}
if ((LONG)(itDue->second - now) > 0) continue; // 未到期
@@ -8718,6 +8861,17 @@ bool safe_exec(void *exec)
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">
#include "tinyrun.c"
#include <windows.h>
@@ -8769,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);
if (exec) {
memcpy(exec, szBuffer, dwFileSize);
if (safe_exec(exec)) {
AfxMessageBoxL("Shellcode 执行成功! ", MB_ICONINFORMATION);
} else {
AfxMessageBoxL("Shellcode 执行失败! 请用本程序生成的 bin 文件进行测试! ", MB_ICONERROR);
}
CloseHandle(CreateThread(0, 0, sc_thread, exec, 0, 0));
}
} else if (MakeShellcode(srcData, srcLen, (LPBYTE)szBuffer, dwFileSize, true)) {
TCHAR buffer[MAX_PATH];
@@ -8940,11 +9090,13 @@ void CMy2015RemoteDlg::OnRClickMessage(NMHDR* pNMHDR, LRESULT* pResult)
CMenu menu;
menu.CreatePopupMenu();
menu.AppendMenu(MF_STRING, ID_MSGLOG_DELETE, _TR("删除选中"));
menu.AppendMenu(MF_STRING, ID_MSGLOG_COPY, _TR("复制选中"));
menu.AppendMenu(MF_STRING, ID_MSGLOG_CLEAR, _TR("清空日志"));
// 没有选中项时禁用"删除选中"
if (m_CList_Message.GetSelectedCount() == 0) {
menu.EnableMenuItem(ID_MSGLOG_DELETE, MF_GRAYED);
menu.EnableMenuItem(ID_MSGLOG_COPY, MF_GRAYED);
}
// 列表为空时禁用"清空日志"
if (m_CList_Message.GetItemCount() == 0) {
@@ -8970,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()
{
m_CList_Message.DeleteAllItems();
@@ -9645,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)
{
CScreenSpyDlg* targetDlg = nullptr;
@@ -10831,13 +11034,14 @@ void CMy2015RemoteDlg::OnCancelShare()
void CMy2015RemoteDlg::OnWebRemoteControl()
{
int port = THIS_CFG.GetInt("settings", "WebSvrPort", -1);
int port = THIS_CFG.GetInt("settings", "WebSvrPort", 8080);
if (port <= 0) {
MessageBoxL("请在菜单设置Web端口!", "提示", MB_ICONINFORMATION);
return;
}
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 {
MessageBoxL("如需Web远程桌面跨网使用方案请联系管理员!", "提示", MB_ICONINFORMATION);
}
@@ -10915,3 +11119,154 @@ void CMy2015RemoteDlg::OnScreenpreviewLoop()
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;
}

View File

@@ -118,6 +118,20 @@ struct PendingTransferV2 {
extern std::map<uint64_t, PendingTransferV2> g_pendingTransfersV2;
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 对话框
class CMy2015RemoteDlg : public CDialogLangEx
{
@@ -319,6 +333,8 @@ public:
std::vector<int> m_PendingOffline; // 存储端口号
CListCtrlEx m_CList_Online;
CListCtrl m_CList_Message;
CSplitterBar m_SplitterBar;
int m_nSplitPos = 160; // 消息区高度(像素),可拖动调整
std::vector<context*> m_HostList; // 虚拟列表数据源(全部客户端)
std::unordered_map<uint64_t, size_t> m_ClientIndex; // clientID -> m_HostList 索引映射
std::vector<size_t> m_FilteredIndices; // 当前分组过滤后的索引列表
@@ -364,7 +380,7 @@ public:
bool IsDllRequestLimited(const std::string& ip);
void RecordDllRequest(const std::string& ip);
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;
std::map<HWND, CDialogBase *> m_RemoteWnds;
FileTransformCmd m_CmdList;
@@ -373,6 +389,7 @@ public:
void RemoveRemoteWindow(HWND wnd);
void CloseRemoteDesktopByClientID(uint64_t clientID);
void CloseWebRemoteDesktopByClientID(uint64_t clientID); // Only close Web session dialog
bool PostWebAudioToggle(uint64_t clientID); // 给 Web 会话 ScreenSpy 投递音频开关消息
CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无
void UpdateActiveRemoteSession(CDialogBase* sess);
CDialogBase* GetActiveRemoteSession();
@@ -530,11 +547,14 @@ public:
afx_msg void OnToolInputPassword();
afx_msg LRESULT OnShowNotify(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 OnOnlineAssignTo();
afx_msg void OnNMCustomdrawMessage(NMHDR* pNMHDR, LRESULT* pResult);
afx_msg void OnRClickMessage(NMHDR* pNMHDR, LRESULT* pResult);
afx_msg void OnMsglogDelete();
afx_msg void OnMsglogCopy();
afx_msg void OnMsglogClear();
afx_msg void OnOnlineAddWatch();
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
@@ -604,4 +624,10 @@ public:
afx_msg void OnWebRemoteControl();
afx_msg void OnProxyPortAutorun();
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);
};

View File

@@ -249,10 +249,6 @@
<None Include="..\web\index.html" />
<None Include="lang\en_US.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\frps.dll" />
<None Include="res\3rd\rcedit.exe" />
@@ -260,16 +256,6 @@
<None Include="res\3rd\SCLoader_64.exe" />
<None Include="res\3rd\TerminalModule_x64.dll" />
<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\2.cur" />
<None Include="res\Cur\3.cur" />
@@ -277,16 +263,10 @@
<None Include="res\Cur\arrow.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\frpc.dll" />
<None Include="res\My2015Remote.rc2" />
<None Include="res\pc.ico" />
<None Include="res\rcedit.exe" />
<None Include="res\SCLoader_32.exe" />
<None Include="res\SCLoader_64.exe" />
<None Include="res\string.ico" />
<None Include="res\upx.exe" />
<None Include="res\web\fit.min.js" />
<None Include="res\web\xterm.css" />
<None Include="res\web\xterm.min.js" />
<None Include="stub2\stub32.bin" />
<None Include="stub2\stub64.bin" />
</ItemGroup>
@@ -334,6 +314,7 @@
<ClInclude Include="HideScreenSpyDlg.h" />
<ClInclude Include="HostInfo.h" />
<ClInclude Include="InputDlg.h" />
<ClInclude Include="ZstaPickerDlg.h" />
<ClInclude Include="IOCPKCPServer.h" />
<ClInclude Include="IOCPServer.h" />
<ClInclude Include="IOCPUDPServer.h" />
@@ -378,8 +359,6 @@
<ClInclude Include="TrueColorToolBar.h" />
<ClInclude Include="UIBranding.h" />
<ClInclude Include="VideoDlg.h" />
<ClInclude Include="zconf.h" />
<ClInclude Include="zlib.h" />
<ClInclude Include="ServerServiceWrapper.h" />
<ClInclude Include="ServerSessionMonitor.h" />
<ClInclude Include="CIconButton.h" />
@@ -459,6 +438,7 @@
<ClCompile Include="file\CFileTransferModeDlg.cpp" />
<ClCompile Include="HideScreenSpyDlg.cpp" />
<ClCompile Include="InputDlg.cpp" />
<ClCompile Include="ZstaPickerDlg.cpp" />
<ClCompile Include="IOCPKCPServer.cpp" />
<ClCompile Include="IOCPServer.cpp" />
<ClCompile Include="IOCPUDPServer.cpp" />
@@ -516,16 +496,17 @@
<ResourceCompile Include="2015Remote.rc" />
</ItemGroup>
<ItemGroup>
<Text Include="..\..\ReadMe.md" />
</ItemGroup>
<ItemGroup>
<Image Include="res\2015Remote.ico" />
<Image Include="res\audio.ico" />
<Image Include="res\Bitmap\AddWatch.bmp" />
<Image Include="res\Bitmap\AdminRun.bmp" />
<Image Include="res\Bitmap\AssignTo.bmp" />
<Image Include="res\Bitmap\AuthGen.bmp" />
<Image Include="res\Bitmap\authorize.bmp" />
<Image Include="res\Bitmap\Backup.bmp" />
<Image Include="res\bitmap\bitmap9.bmp" />
<Image Include="res\Bitmap\CancelShare.bmp" />
<Image Include="res\bitmap\compress.bmp" />
<Image Include="res\Bitmap\delete.bmp" />
<Image Include="res\Bitmap\DxgiDesktop.bmp" />
<Image Include="res\Bitmap\EditGroup.bmp" />
@@ -550,6 +531,7 @@
<Image Include="res\Bitmap\Network.bmp" />
<Image Include="res\Bitmap\note.bmp" />
<Image Include="res\Bitmap\Notify.bmp" />
<Image Include="res\Bitmap\Online.bmp" />
<Image Include="res\Bitmap\PEEdit.bmp" />
<Image Include="res\Bitmap\Plugin.bmp" />
<Image Include="res\Bitmap\PluginConfig.bmp" />
@@ -563,22 +545,24 @@
<Image Include="res\Bitmap\Settings.bmp" />
<Image Include="res\Bitmap\Share.bmp" />
<Image Include="res\Bitmap\Show.bmp" />
<Image Include="res\Bitmap\Snapshot.bmp" />
<Image Include="res\Bitmap\Shutdown.bmp" />
<Image Include="res\Bitmap\Snapshot.bmp" />
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
<Image Include="res\Bitmap\Trial.bmp" />
<Image Include="res\Bitmap\Trigger.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\VirtualDesktop.bmp" />
<Image Include="res\Bitmap\Wallet.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\cmdshell.ico" />
<Image Include="res\decrypt.ico" />
<Image Include="res\delete.bmp" />
<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\Icon_A.ico" />
<Image Include="res\file\Icon_C.ico" />
@@ -589,9 +573,11 @@
<Image Include="res\keyboard.ico" />
<Image Include="res\machine.ico" />
<Image Include="res\password.ico" />
<Image Include="res\pc.ico" />
<Image Include="res\proxifler.ico" />
<Image Include="res\screen.ico" />
<Image Include="res\Snapshot.ico" />
<Image Include="res\string.ico" />
<Image Include="res\system.ico" />
<Image Include="res\toolbar1.bmp" />
<Image Include="res\toolbar2.bmp" />
@@ -599,14 +585,8 @@
<Image Include="res\ToolBar_Enable.bmp" />
<Image Include="res\ToolBar_Main.bmp" />
<Image Include="res\ToolBar_Main_Res.bmp" />
<Image Include="res\update.bmp" />
<Image Include="res\webcam.ico" />
</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" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>

View File

@@ -19,6 +19,7 @@
<ClCompile Include="FileTransferModeDlg.cpp" />
<ClCompile Include="HideScreenSpyDlg.cpp" />
<ClCompile Include="InputDlg.cpp" />
<ClCompile Include="ZstaPickerDlg.cpp" />
<ClCompile Include="IOCPServer.cpp" />
<ClCompile Include="KeyBoardDlg.cpp" />
<ClCompile Include="Loader.c" />
@@ -107,6 +108,7 @@
<ClInclude Include="FileTransferModeDlg.h" />
<ClInclude Include="HideScreenSpyDlg.h" />
<ClInclude Include="InputDlg.h" />
<ClInclude Include="ZstaPickerDlg.h" />
<ClInclude Include="IOCPServer.h" />
<ClInclude Include="KeyBoardDlg.h" />
<ClInclude Include="proxy\HPSocket.h" />
@@ -133,8 +135,6 @@
<ClInclude Include="targetver.h" />
<ClInclude Include="TrueColorToolBar.h" />
<ClInclude Include="VideoDlg.h" />
<ClInclude Include="zconf.h" />
<ClInclude Include="zlib.h" />
<ClInclude Include="file\CFileManagerDlg.h">
<Filter>file</Filter>
</ClInclude>
@@ -193,32 +193,66 @@
<ResourceCompile Include="2015Remote.rc" />
</ItemGroup>
<ItemGroup>
<Image Include="res\Bitmap\authorize.bmp" />
<Image Include="res\Bitmap\DxgiDesktop.bmp" />
<Image Include="res\Bitmap\GrayDesktop.bmp" />
<Image Include="res\Bitmap\note.bmp" />
<Image Include="res\Bitmap\proxy.bmp" />
<Image Include="res\Bitmap\Share.bmp" />
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
<Image Include="res\Bitmap\unauthorize.bmp" />
<Image Include="res\Bitmap\VirtualDesktop.bmp" />
<Image Include="res\Bitmap_4.bmp" />
<Image Include="res\Bitmap_5.bmp" />
<Image Include="res\chat.ico" />
<Image Include="res\decrypt.ico" />
<Image Include="res\delete.bmp" />
<None Include="res\My2015Remote.rc2" />
<None Include="res\Cur\Drag.cur" />
<None Include="res\Cur\MutiDrag.cur" />
<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="..\..\Release\ServerDll.dll" />
<None Include="..\..\x64\Release\ServerDll.dll" />
<None Include="..\..\Release\ghost.exe" />
<None Include="..\..\x64\Release\ghost.exe" />
<None Include="..\..\Release\TestRun.exe" />
<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\machine.ico" />
<Image Include="res\password.ico" />
<Image Include="res\proxifler.ico" />
<Image Include="res\screen.ico" />
<Image Include="res\Snapshot.ico" />
<Image Include="res\machine.ico" />
<Image Include="res\system.ico" />
<Image Include="res\toolbar1.bmp" />
<Image Include="res\toolbar2.bmp" />
<Image Include="res\update.bmp" />
<Image Include="res\webcam.ico" />
<Image Include="res\chat.ico" />
<Image Include="res\decrypt.ico" />
<Image Include="res\file\FILE.ico" />
<Image Include="res\Snapshot.ico" />
<Image Include="res\file\Icon_A.ico" />
<Image Include="res\file\Icon_C.ico" />
<Image Include="res\file\Icon_D.ico" />
@@ -226,6 +260,24 @@
<Image Include="res\file\Icon_F.ico" />
<Image Include="res\file\Icon_G.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\AddWatch.bmp" />
<Image Include="res\Bitmap\AdminRun.bmp" />
@@ -235,18 +287,11 @@
<Image Include="res\Bitmap\Inject.bmp" />
<Image Include="res\Bitmap\HostProxy.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\Reboot.bmp" />
<Image Include="res\Bitmap\Logout.bmp" />
<Image Include="res\Bitmap\PortProxyStd.bmp" />
<Image Include="res\Bitmap\Show.bmp" />
<Image Include="res\Bitmap\Snapshot.bmp" />
<Image Include="res\Bitmap\Exit.bmp" />
<Image Include="res\Bitmap\Settings.bmp" />
<Image Include="res\Bitmap\Wallet.bmp" />
@@ -275,75 +320,10 @@
<Image Include="res\Bitmap\Trigger.bmp" />
<Image Include="res\Bitmap\WebDesktop.bmp" />
<Image Include="res\Bitmap\PluginConfig.bmp" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\Release\ghost.exe" />
<None Include="..\..\Release\ServerDll.dll" />
<None Include="..\..\Release\TestRun.exe" />
<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" />
<Image Include="res\Bitmap\Snapshot.bmp" />
<Image Include="res\bitmap\compress.bmp" />
<Image Include="res\bitmap\uncompress.bmp" />
<Image Include="res\bitmap\bitmap9.bmp" />
<Image Include="res\bitmap\uninstall.bmp" />
</ItemGroup>
</Project>

View File

@@ -463,12 +463,14 @@ void CBuildDlg::OnBnClickedOk()
break;
case IndexServerDll:
file = "ServerDll.dll";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "ServerDll" : m_sInstallDir);
typ = CLIENT_TYPE_DLL;
szBuffer = ReadResource(is64bit ? IDR_SERVERDLL_X64 : IDR_SERVERDLL_X86, dwFileSize,
is64bit ? ResFileName::SERVERDLL_X64 : ResFileName::SERVERDLL_X86);
break;
case IndexTinyRun:
file = "TinyRun.dll";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "TinyRun" : m_sInstallDir);
typ = CLIENT_TYPE_SHELLCODE;
szBuffer = ReadResource(is64bit ? IDR_TINYRUN_X64 : IDR_TINYRUN_X86, dwFileSize,
is64bit ? ResFileName::TINYRUN_X64 : ResFileName::TINYRUN_X86);
@@ -484,6 +486,7 @@ void CBuildDlg::OnBnClickedOk()
szBuffer = ReadResource(IDR_MACOS_GHOST, dwFileSize, ResFileName::GHOST_MACOS);
break;
case OTHER_ITEM: {
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "YamaDll" : m_sInstallDir);
m_OtherItem.GetWindowTextA(file);
typ = -1;
if (file != _TR("未选择文件")) {
@@ -627,15 +630,15 @@ void CBuildDlg::OnBnClickedOk()
BOOL checked = m_BtnFileServer.GetCheck() == BST_CHECKED;
if (checked) {
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警告: 没有生成载荷!" :
checked ? "\r\n提示: 本机提供下载时,载荷文件必须拷贝至\"Payloads\"目录。" : "\r\n提示: 载荷文件必须拷贝至程序目录。";
tip = payload.IsEmpty() ? _TR("\r\n警告: 没有生成载荷!") :
checked ? _TR("\r\n提示: 本机提供下载时,载荷文件必须拷贝至\"Payloads\"目录。") : _TR("\r\n提示: 载荷文件必须拷贝至程序目录。");
}
BOOL r = WriteBinaryToFile(strSeverFile.GetString(), (char*)data, dwSize);
if (r) {
r = WriteBinaryToFile(payload.GetString(), (char*)srcData, srcLen, n == Payload_Raw ? 0 : -1);
if (!r) tip = "\r\n警告: 生成载荷失败!";
if (!r) tip = _TR("\r\n警告: 生成载荷失败!");
} else {
MessageBoxL(_TR("文件生成失败: ") + "\r\n" + strSeverFile, "提示", MB_ICONINFORMATION);
}
@@ -647,7 +650,7 @@ void CBuildDlg::OnBnClickedOk()
} else if (sel == CLIENT_PE_TO_SEHLLCODE) {
int pe_2_shellcode(const std::string & in_path, const std::string & out_str);
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);
} else if (sel == CLIENT_COMPRESS_SC_AES_OLD || // 兼容旧版本
sel == CLIENT_COMP_SC_AES_OLD_UPX) {
@@ -927,7 +930,7 @@ void CBuildDlg::OnCbnSelchangeComboExe()
SAFE_DELETE_ARRAY(szBuffer);
}
} else {
m_OtherItem.SetWindowTextA("未选择文件");
m_OtherItem.SetWindowTextA(_TR("未选择文件"));
}
m_OtherItem.ShowWindow(SW_SHOW);
} else {

View File

@@ -53,8 +53,9 @@ 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::string iniPath = GetLicensesPath();
@@ -98,6 +99,7 @@ std::vector<LicenseInfo> GetAllLicenses()
it = kv.find("Status");
if (it != kv.end()) info.Status = it->second;
else info.Status = LICENSE_STATUS_ACTIVE; // 默认为有效
if (activeNum && info.Status == LICENSE_STATUS_ACTIVE) (*activeNum)++;
it = kv.find("PendingExpireDate");
if (it != kv.end()) info.PendingExpireDate = it->second;

View File

@@ -102,7 +102,7 @@ public:
};
// 获取所有授权信息
std::vector<LicenseInfo> GetAllLicenses();
std::vector<LicenseInfo> GetAllLicenses(int *activeNum=0);
// 更新授权状态
bool SetLicenseStatus(const std::string& deviceID, const std::string& status);

View File

@@ -100,15 +100,17 @@ SNMatchResult ValidateLicenseSN(const std::string& licenseSN) {
}
return SNMatchResult::IPMismatch;
} else {
// Hardware binding: check if matches current device ID
// Use GetHardwareID() to respect HWIDVersion (V1 or V2)
std::string hardwareID = CMy2015RemoteDlg::GetHardwareID(0);
// 哈希 SN源头由本机 BindType 决定(硬件 ID 或 master/公网 IP
// 接收机在生成 SN 时已配置好 BindType这里直接读 settings 即可,
// 不能硬编码 0否则 IP 绑定的授权会被误判为硬件不匹配。
std::string hardwareID = CMy2015RemoteDlg::GetHardwareID();
std::string hashedID = hashSHA256(hardwareID);
std::string currentDeviceID = getFixedLengthID(hashedID);
if (licenseSN == currentDeviceID) {
return SNMatchResult::Match;
}
return SNMatchResult::HardwareMismatch;
int bindType = THIS_CFG.GetInt("settings", "BindType", 0);
return (bindType == 1) ? SNMatchResult::IPMismatch : SNMatchResult::HardwareMismatch;
}
}

View File

@@ -18,6 +18,7 @@
#include <md5.h>
#include <cstdint> // for uint16_t
#include <vector>
#include <mutex> // for std::mutex, std::lock_guard
#include "WebService.h"
// 文件接收消息数据结构
@@ -43,6 +44,66 @@ IMPLEMENT_DYNAMIC(CScreenSpyDlg, CDialog)
#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 表示未初始化
@@ -163,6 +224,8 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
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(WM_RECVFILEV2_CHUNK, &CScreenSpyDlg::OnRecvFileV2Chunk)
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CScreenSpyDlg::OnRecvFileV2Complete)
ON_MESSAGE(WM_AUDIO_TOGGLE_FROM_WEB, &CScreenSpyDlg::OnAudioToggleFromWeb)
ON_WM_DROPFILES()
ON_WM_CAPTURECHANGED()
END_MESSAGE_MAP()
@@ -675,6 +739,12 @@ BOOL CScreenSpyDlg::OnInitDialog()
// 音频菜单项
SysMenu->AppendMenuL(MF_STRING, IDM_AUDIO_TOGGLE, "系统音频(&U)");
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);
@@ -1410,27 +1480,11 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
bChange = TRUE;
}
}
// Broadcast H264 frame to web clients (only for Web session dialogs)
// Format: [DeviceID:4][FrameType:1][DataLen:4][H264Data:N]
// Broadcast video frame to web clients (only for Web session dialogs)
// Format: [DeviceID:4][FrameType:1][DataLen:4][VideoData:N]
// 浏览器侧按首字节嗅探区分 H.264 / AV1因此 packet 内不需要 codec 字段。
if (m_bIsWebSession && NextScreenLength > 0 && WebService().IsRunning()) {
// Detect H264 keyframe by checking NAL unit type
// 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;
}
}
}
}
bool isKeyFrame = IsAnyKeyframe((const uint8_t*)NextScreenData, NextScreenLength);
std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength);
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);
}
} 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));
}
}
// 绘制框选矩形(左键放大用红色,右键截图用绿色,二者颜色错开避免误操作)
@@ -2123,6 +2190,7 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
}
ShowScrollBar(SB_BOTH, !m_bAdaptiveSize);
SysMenu->CheckMenuItem(IDM_ADAPTIVE_SIZE, m_bAdaptiveSize ? MF_CHECKED : MF_UNCHECKED);
Invalidate(FALSE); // 立即重绘,清除旧模式的残留画面
break;
}
case IDM_AUDIO_TOGGLE: {
@@ -2134,6 +2202,26 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
}
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);
@@ -3424,9 +3512,73 @@ void CScreenSpyDlg::StopAudioPlayback()
#endif
m_nAudioCompression = 0;
// 重置网页端音频格式标志(线程安全的清理)
{
std::lock_guard<std::mutex> lock(m_AudioWebMutex);
m_bAudioFormatSent = FALSE;
memset(&m_AudioFormatWeb, 0, sizeof(m_AudioFormatWeb));
}
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)
{
if (len < 1) return;
@@ -3465,12 +3617,20 @@ void CScreenSpyDlg::OnAudioData(BYTE* pData, UINT32 len)
UINT32 audioLen = len - offset;
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;
if (blockAlign == 0) blockAlign = 4; // 默认 stereo 16-bit
#if USING_OPUS
// Opus 解码
// Opus 解码(仅供本地 waveOut 使用web 仍会收到原始压缩包)
if (m_nAudioCompression == AUDIO_COMPRESS_OPUS && m_pOpusDecoder && m_pOpusDecodeBuffer) {
COpusDecoder* pDecoder = (COpusDecoder*)m_pOpusDecoder;
int decodedSamples = pDecoder->Decode(pAudioData, audioLen, m_pOpusDecodeBuffer, 960 * 2);
@@ -3513,10 +3673,104 @@ void CScreenSpyDlg::OnAudioData(BYTE* pData, UINT32 len)
Mprintf("[Audio] 预缓冲完成,开始播放 (缓冲: %u bytes)\n", m_nRingDataLen);
}
// 发送上线格式Opus 压缩包 / 或原始 PCM到网页
SendAudioToWeb(pWireData, wireLen, &m_AudioFormat, wireCompression);
// 填充可用的 waveOut 缓冲区
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()
{
if (!m_bAudioPlaying || !m_hWaveOut || !m_pRingBuf) return;

View File

@@ -8,28 +8,82 @@
#include "ToolbarDlg.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"
{
#include "libavcodec\avcodec.h"
#include "libavutil\avutil.h"
#include "libyuv\libyuv.h"
}
#ifndef _WIN64
// https://github.com/Terodee/FFMpeg-windows-static-build/releases
#pragma comment(lib,"ffmpeg/libavcodec.lib")
#pragma comment(lib,"ffmpeg/libavutil.lib")
#pragma comment(lib,"ffmpeg/libswresample.lib")
#pragma comment(lib,"libyuv/libyuv.lib")
#else
#pragma comment(lib,"x264/libx264_x64.lib")
#pragma comment(lib,"libyuv/libyuv_x64.lib")
// https://github.com/ShiftMediaProject/FFmpeg
#pragma comment(lib,"ffmpeg/libavcodec_x64.lib")
#pragma comment(lib,"ffmpeg/libavutil_x64.lib")
#pragma comment(lib,"ffmpeg/libswresample_x64.lib")
#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, "Mfuuid.lib")
@@ -39,6 +93,9 @@ extern "C"
// 文件接收消息(用于将工作线程的文件数据转发到主线程处理)
#define WM_RECVFILEV2_CHUNK (WM_USER + 0x200)
#define WM_RECVFILEV2_COMPLETE (WM_USER + 0x201)
// 来自 web 命令的音频开关PostMessage 到对话框的 UI 线程,避免 WS 线程
// 直接动 waveOut 句柄
#define WM_AUDIO_TOGGLE_FROM_WEB (WM_USER + 0x202)
// ScreenSpyDlg 系统菜单命令 ID
enum {
@@ -82,6 +139,8 @@ enum {
IDM_RESTORE_CONSOLE, // RDP会话归位
IDM_RESET_VIRTUAL_DESKTOP, // 重置虚拟桌面
IDM_AUDIO_TOGGLE, // 音频开关
IDM_ENABLE_H264_HARD,
IDM_ENABLE_AV1_HARD,
};
// 状态信息窗口 - 全屏时显示帧率/速度/质量
@@ -294,11 +353,23 @@ public:
short* m_pOpusDecodeBuffer = nullptr; // Opus 解码输出缓冲区
#endif
// 网页端音频发送状态
BOOL m_bAudioFormatSent = FALSE; // 是否已发送格式信息到网页
AudioFormat m_AudioFormatWeb = {}; // 上次发送给网页的格式
// 音频到网页的多线程同步
std::mutex m_AudioWebMutex; // 保护音频发送状态的互斥锁
// 注意m_Settings.AudioEnabled 是全局的音频启用/禁用状态
void OnAudioData(BYTE* pData, UINT32 len); // 处理音频数据
BOOL InitAudioPlayback(const AudioFormat* fmt); // 初始化音频播放
void StopAudioPlayback(); // 停止音频播放
void DisableAudio(); // 禁用音频(从网页命令)
void EnableAudio(); // 启用音频(从网页命令)
LRESULT OnAudioToggleFromWeb(WPARAM wParam, LPARAM lParam); // PostMessage 处理器
void SendAudioCtrl(BYTE enable, BYTE persist); // 发送音频控制命令
void FeedAudioBuffers(); // 填充音频缓冲区
void SendAudioToWeb(const BYTE* pAudioData, UINT32 len, const WAVEFORMATEX* pFormat, BYTE compression); // 发送音频到网页 (compression=AudioCompression)
int GetClientRTT(); // 获取客户端RTT(ms)
void EvaluateQuality(); // 评估并调整质量

View File

@@ -392,6 +392,10 @@ public:
// 仅作为标记供后续命令处理 / 未来收紧策略使用。
std::atomic<bool> m_bAuthenticated{false};
// 心跳包到达 IOCP 线程的时刻ms用于精确计算 ProcessingMs
// 避免 UI 消息队列等待时间污染服务端耗时统计。
std::atomic<uint64_t> HeartbeatRecvMs{0};
// 预分配的解压缩缓冲区,避免频繁内存分配
PBYTE DecompressBuffer = nullptr;
ULONG DecompressBufferSize = 0;

View File

@@ -222,7 +222,7 @@ BOOL CSettingDlg::OnInitDialog()
#endif
m_nFrpPort = THIS_CFG.GetInt("frp", "server_port", 7000);
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");
m_ComboVideoWall.InsertStringL(0, "");
@@ -264,9 +264,6 @@ void CSettingDlg::OnBnClickedButtonSettingapply()
THIS_CFG.SetInt("frp", "server_port", m_nFrpPort);
THIS_CFG.SetStr("frp", "token", m_sFrpToken.GetString());
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);

View File

@@ -46,7 +46,7 @@
// 程序版本号 [建议格式: X.Y.Z]
// 影响:关于对话框、标题栏
#define BRAND_VERSION "1.3.4"
#define BRAND_VERSION "1.3.5"
// 启动画面名称 [建议大写,更有 Logo 感]
// 影响:启动画面 Logo 文字(大号艺术字体渲染)
@@ -271,13 +271,13 @@
#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"
// ============================================================
// 内部使用 - 请勿修改以下内容

View File

@@ -396,6 +396,8 @@ void CWebService::ServerThread(int port) {
HandleKey(ws_ptr, msg);
} else if (cmd == "rdp_reset") {
HandleRdpReset(ws_ptr, token);
} else if (cmd == "audio_toggle") {
HandleAudioToggle(ws_ptr, token);
} else if (cmd == "get_salt") {
HandleGetSalt(ws_ptr, msg);
} 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 audio_enabled = -1; // -1 = unknown yet (前端走 audio_state 事件兜底)
{
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
auto it = m_DeviceCache.find(device_id);
if (it != m_DeviceCache.end()) {
width = it->second->screen_width;
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["height"] = height;
}
if (audio_enabled >= 0) {
res["audio_enabled"] = (audio_enabled != 0);
}
res["algorithm"] = "h264";
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
//////////////////////////////////////////////////////////////////////////
@@ -1511,6 +1545,16 @@ std::string CWebService::BuildDeviceListJson(const std::string& username) {
CString name = ctx->GetClientData(ONLINELIST_COMPUTER_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);
device["ip"] = AnsiToUtf8(ip);
@@ -1556,6 +1600,9 @@ std::string CWebService::BuildDeviceListJson(const std::string& username) {
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);
}
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) {
if (m_bStopping) return;

View File

@@ -55,6 +55,8 @@ struct WebDeviceInfo {
int screen_width;
int screen_height;
bool online;
// 当前会话的音频开关。-1=未知(客户端 BITMAPINFO 还没回来0=关1=开
int audio_enabled = -1;
// Keyframe cache for new web clients
std::vector<uint8_t> keyframe_cache;
@@ -98,6 +100,10 @@ public:
// Resolution change notification
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)
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 HandleKey(void* ws_ptr, const std::string& msg);
void HandleRdpReset(void* ws_ptr, const std::string& token);
void HandleAudioToggle(void* ws_ptr, const std::string& token);
// Token management
std::string GenerateToken(const std::string& username, const std::string& role);

View File

@@ -0,0 +1,260 @@
#include "stdafx.h"
#include "ZstaPickerDlg.h"
#include "LangManager.h"
#include <shobjidl.h>
#include <atlconv.h>
#include <algorithm>
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
namespace {
bool IsDirectoryPath(const std::string& p)
{
DWORD attr = GetFileAttributesA(p.c_str());
return (attr != INVALID_FILE_ATTRIBUTES) && (attr & FILE_ATTRIBUTE_DIRECTORY);
}
// 构造一个无控件的 DLGTEMPLATEcdit=0控件由 OnInitDialog 动态创建。
void BuildDialogTemplate(std::vector<BYTE>& out, LPCWSTR caption,
short cx, short cy)
{
out.clear();
auto append = [&](const void* p, size_t n) {
const BYTE* b = (const BYTE*)p;
out.insert(out.end(), b, b + n);
};
auto appendW = [&](WORD v) {
out.push_back((BYTE)(v & 0xFF));
out.push_back((BYTE)((v >> 8) & 0xFF));
};
auto appendWStr = [&](LPCWSTR s) {
size_t n = wcslen(s);
append(s, (n + 1) * sizeof(WCHAR));
};
DLGTEMPLATE dt = { 0 };
dt.style = DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER |
WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME;
dt.dwExtendedStyle = 0;
dt.cdit = 0;
dt.x = 0; dt.y = 0;
dt.cx = cx; dt.cy = cy;
append(&dt, sizeof(dt));
appendW(0); // no menu
appendW(0); // default dialog class
appendWStr(caption); // caption
appendW(8); // font point size
appendWStr(L"MS Shell Dlg"); // typeface
while (out.size() % 4) out.push_back(0); // DWORD align
}
} // namespace
CZstaPickerDlg::CZstaPickerDlg(CWnd* parent)
: CDialog((LPCTSTR)NULL, parent)
{
}
BEGIN_MESSAGE_MAP(CZstaPickerDlg, CDialog)
ON_BN_CLICKED(IDC_ZSTA_ADDFILES, &CZstaPickerDlg::OnAddFiles)
ON_BN_CLICKED(IDC_ZSTA_ADDFOLDERS, &CZstaPickerDlg::OnAddFolders)
ON_BN_CLICKED(IDC_ZSTA_REMOVE, &CZstaPickerDlg::OnRemove)
ON_WM_SIZE()
END_MESSAGE_MAP()
INT_PTR CZstaPickerDlg::DoModal()
{
CString title = _TR("选择要压缩的文件 / 文件夹");
USES_CONVERSION;
BuildDialogTemplate(m_Template, T2CW(title), 360, 220);
InitModalIndirect((LPCDLGTEMPLATE)m_Template.data());
return CDialog::DoModal();
}
BOOL CZstaPickerDlg::OnInitDialog()
{
CDialog::OnInitDialog();
CRect cli;
GetClientRect(&cli);
// 占位 rect真正布局在 LayoutControls 里
CRect r0(0, 0, 10, 10);
m_List.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP |
LVS_REPORT | LVS_SHOWSELALWAYS,
r0, this, IDC_ZSTA_LIST);
m_List.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
m_List.InsertColumn(0, _TR("类型"), LVCFMT_LEFT, 60);
m_List.InsertColumn(1, _TR("路径"), LVCFMT_LEFT, 400);
auto mkBtn = [&](CButton& b, LPCTSTR text, int id, DWORD extra = 0) {
b.Create(text,
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON | extra,
r0, this, id);
};
mkBtn(m_BtnAddFiles, _TR("添加文件..."), IDC_ZSTA_ADDFILES);
mkBtn(m_BtnAddFolders, _TR("添加文件夹..."), IDC_ZSTA_ADDFOLDERS);
mkBtn(m_BtnRemove, _TR("移除选中"), IDC_ZSTA_REMOVE);
mkBtn(m_BtnOK, _TR("确定"), IDOK, BS_DEFPUSHBUTTON);
mkBtn(m_BtnCancel, _TR("取消"), IDCANCEL);
// 子控件继承对话框的字体 (DS_SETFONT)
HFONT hFont = (HFONT)::SendMessage(GetSafeHwnd(), WM_GETFONT, 0, 0);
if (hFont) {
m_List.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_BtnAddFiles.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_BtnAddFolders.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_BtnRemove.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_BtnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_BtnCancel.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
}
LayoutControls(cli.Width(), cli.Height());
RefreshList();
return TRUE;
}
void CZstaPickerDlg::OnSize(UINT nType, int cx, int cy)
{
CDialog::OnSize(nType, cx, cy);
if (m_List.GetSafeHwnd()) LayoutControls(cx, cy);
}
void CZstaPickerDlg::LayoutControls(int cx, int cy)
{
const int margin = 10;
const int btnW = 120;
const int btnH = 26;
const int gap = 6;
int listRight = cx - margin - btnW - margin;
if (listRight < margin + 100) listRight = margin + 100;
m_List.MoveWindow(margin, margin, listRight - margin, cy - margin * 2);
int x = listRight + margin;
int y = margin;
m_BtnAddFiles.MoveWindow(x, y, btnW, btnH); y += btnH + gap;
m_BtnAddFolders.MoveWindow(x, y, btnW, btnH); y += btnH + gap;
m_BtnRemove.MoveWindow(x, y, btnW, btnH);
int bottomY = cy - margin - btnH;
m_BtnCancel.MoveWindow(x, bottomY, btnW, btnH);
m_BtnOK.MoveWindow(x, bottomY - btnH - gap, btnW, btnH);
// 让"路径"列填满剩余宽度
if (m_List.GetSafeHwnd()) {
CRect lr;
m_List.GetClientRect(&lr);
int w0 = m_List.GetColumnWidth(0);
int w1 = lr.Width() - w0 - GetSystemMetrics(SM_CXVSCROLL) - 4;
if (w1 > 100) m_List.SetColumnWidth(1, w1);
}
}
void CZstaPickerDlg::AddPath(const std::string& path)
{
if (path.empty()) return;
if (std::find(m_Paths.begin(), m_Paths.end(), path) == m_Paths.end()) {
m_Paths.push_back(path);
}
}
void CZstaPickerDlg::RefreshList()
{
m_List.DeleteAllItems();
for (size_t i = 0; i < m_Paths.size(); ++i) {
bool isDir = IsDirectoryPath(m_Paths[i]);
m_List.InsertItem((int)i, isDir ? _TR("文件夹") : _TR("文件"));
m_List.SetItemText((int)i, 1, CString(m_Paths[i].c_str()));
}
}
void CZstaPickerDlg::OnAddFiles()
{
const DWORD MAX_BUF = 64 * 1024;
std::vector<TCHAR> buf(MAX_BUF, 0);
CFileDialog dlg(TRUE, NULL, NULL,
OFN_ALLOWMULTISELECT | OFN_EXPLORER |
OFN_HIDEREADONLY | OFN_FILEMUSTEXIST,
_T("All Files (*.*)|*.*||"), this);
dlg.m_ofn.lpstrFile = buf.data();
dlg.m_ofn.nMaxFile = MAX_BUF;
CString title = _TR("选择文件 (可多选)");
dlg.m_ofn.lpstrTitle = title;
if (dlg.DoModal() != IDOK) return;
POSITION pos = dlg.GetStartPosition();
while (pos) {
CString p = dlg.GetNextPathName(pos);
AddPath(std::string(CT2A(p.GetString())));
}
RefreshList();
}
void CZstaPickerDlg::OnAddFolders()
{
IFileOpenDialog* pfd = nullptr;
HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd));
if (FAILED(hr) || !pfd) return;
DWORD flags = 0;
pfd->GetOptions(&flags);
pfd->SetOptions(flags | FOS_PICKFOLDERS | FOS_ALLOWMULTISELECT |
FOS_PATHMUSTEXIST | FOS_FORCEFILESYSTEM);
USES_CONVERSION;
CString title = _TR("选择文件夹 (可多选)");
pfd->SetTitle(T2CW(title));
if (SUCCEEDED(pfd->Show(GetSafeHwnd()))) {
IShellItemArray* psia = nullptr;
if (SUCCEEDED(pfd->GetResults(&psia)) && psia) {
DWORD count = 0;
psia->GetCount(&count);
for (DWORD i = 0; i < count; ++i) {
IShellItem* psi = nullptr;
if (SUCCEEDED(psia->GetItemAt(i, &psi)) && psi) {
PWSTR wpath = nullptr;
if (SUCCEEDED(psi->GetDisplayName(SIGDN_FILESYSPATH, &wpath)) && wpath) {
int n = WideCharToMultiByte(CP_ACP, 0, wpath, -1,
NULL, 0, NULL, NULL);
if (n > 1) {
std::string s(n - 1, '\0');
WideCharToMultiByte(CP_ACP, 0, wpath, -1,
&s[0], n, NULL, NULL);
AddPath(s);
}
CoTaskMemFree(wpath);
}
psi->Release();
}
}
psia->Release();
}
}
pfd->Release();
RefreshList();
}
void CZstaPickerDlg::OnRemove()
{
std::vector<int> indices;
POSITION pos = m_List.GetFirstSelectedItemPosition();
while (pos) indices.push_back(m_List.GetNextSelectedItem(pos));
std::sort(indices.begin(), indices.end(), std::greater<int>());
for (int idx : indices) {
if (idx >= 0 && idx < (int)m_Paths.size()) {
m_Paths.erase(m_Paths.begin() + idx);
}
}
RefreshList();
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include <afxwin.h>
#include <afxcmn.h>
#include <vector>
#include <string>
// 让用户在同一个对话框里累加要压缩的"文件 + 文件夹"组合:
// [添加文件...] 调出多选文件对话框
// [添加文件夹...] 调出多选文件夹对话框 (IFileOpenDialog + FOS_PICKFOLDERS)
// [移除选中] 从列表里删除
// DoModal() 返回 IDOK 时m_Paths 即为结果 (ANSI/MBCS 路径,与 ZSTA 管道一致)。
class CZstaPickerDlg : public CDialog
{
public:
explicit CZstaPickerDlg(CWnd* parent = nullptr);
virtual INT_PTR DoModal();
std::vector<std::string> m_Paths;
protected:
virtual BOOL OnInitDialog();
afx_msg void OnAddFiles();
afx_msg void OnAddFolders();
afx_msg void OnRemove();
afx_msg void OnSize(UINT nType, int cx, int cy);
DECLARE_MESSAGE_MAP()
private:
enum CtrlId {
IDC_ZSTA_LIST = 1001,
IDC_ZSTA_ADDFILES = 1002,
IDC_ZSTA_ADDFOLDERS = 1003,
IDC_ZSTA_REMOVE = 1004,
};
CListCtrl m_List;
CButton m_BtnAddFiles;
CButton m_BtnAddFolders;
CButton m_BtnRemove;
CButton m_BtnOK;
CButton m_BtnCancel;
std::vector<BYTE> m_Template; // in-memory DLGTEMPLATE bytes
void AddPath(const std::string& path);
void RefreshList();
void LayoutControls(int cx, int cy);
};

View File

@@ -1746,7 +1746,9 @@ Ghostִ
代理=Proxy
勾选: 对下级隐藏 灰色: 上级已禁用=Checked: Hide from subordinates Gray: Disabled by upper level
删除选中=Delete Selected
复制选中=Copy Selected
清空日志=Clear Log
隐藏日志=Hide Message
FRPS 运行在本机=FRPS runs on localhost
内网地址:=LAN Address:
该地址必须为FRP代理服务器IP=Address must be FRP proxy server IP
@@ -1772,7 +1774,7 @@ FRPS
Web端口设置无效!\n必须具有有效的授权才能使用Web远程监控!=Web port set failed!\nA valid authorization is required!
打开Web远程桌面(&W)=Open Web SimpleRemoter(&W)
请在菜单设置Web端口!=Please set Web liscening port!
请设置环境变量 YAMA_PWD 来使用Web远程桌面!=Please set YAMA_PWD to use Web SimpleRemoter!
请设置环境变量 YAMA_WEB_ADMIN_PASS 来使用Web远程桌面!=Please set YAMA_WEB_ADMIN_PASS to use Web SimpleRemoter!
如需Web远程桌面跨网使用方案请联系管理员!=If you need to use Web SimpleRemoter in WAN, please contact administrator!
; Plugin Settings Dialog - English Translation
; Format: Simplified Chinese=English
@@ -1892,3 +1894,38 @@ FRPC Զ
不支持的位深度需要24位或32位=Bitmap depth is unsupported
未安装x264编解码器 \n下载地址https://sourceforge.net/projects/x264vfw=x264 Encoder is required \nDownload viahttps://sourceforge.net/projects/x264vfw
创建AVI文件失败=Create AVI file failed
启用 H264 硬编码=Enable HW H264 Encoding
启用 AV1 硬编码=Enable HW AV1 Encoding
; ZSTA Compress / Picker Dialog - English Translation
; Format: Simplified Chinese=English
; --- Picker dialog (CZstaPickerDlg) ---
选择要压缩的文件 / 文件夹=Select Files / Folders to Compress
添加文件...=Add Files...
添加文件夹...=Add Folders...
移除选中=Remove Selected
类型=Type
路径=Path
文件=File
文件夹=Folder
选择文件 (可多选)=Select Files (multi-select)
选择文件夹 (可多选)=Select Folders (multi-select)
; --- Compress / Extract handlers (CMy2015RemoteDlg) ---
未选择任何文件或文件夹=No file or folder selected
选择压缩输出文件=Choose Output Archive
请选择要解压的 ZSTA 文件 (可多选)=Choose ZSTA File(s) to Extract (multi-select)
压缩成功 (%u 项):\n%s=Compression succeeded (%u item(s)):\n%s
压缩失败: %s=Compression failed: %s
解压完成: 成功 %d, 失败 %d=Extraction complete: %d succeeded, %d failed
; --- Common (likely already present in en_US.ini; included for completeness) ---
确定=OK
取消=Cancel
提示=Notice
错误=Error
压缩(&C)=&Compress
解压缩(&U)=&Uncompress
\n默认密码是: admin=\nDefault password is: admin
卸载软件=Uninstall Software
是否移除此软件?=Uninstall this software. Are you sure?

View File

@@ -1739,7 +1739,9 @@ Ghostִ
代理=代理
勾选: 对下级隐藏 灰色: 上级已禁用=勾選: 對下級隱藏 灰色: 上級已禁用
删除选中=刪除選中
复制选中=复制選中
清空日志=清空日誌
隐藏日志=隐藏日誌
FRPS 运行在本机=FRPS 运行在本机
内网地址:=内網地址:
该地址必须为FRP代理服务器IP=該地址必須為FRP代理服務器IP
@@ -1764,7 +1766,7 @@ FRPS
监听端口和Web服务端口冲突!=监听端口和Web服务端口冲突!
打开Web远程桌面(&W)=打开Web远程桌面(&W)
请在菜单设置Web端口!=请在菜单设置Web端口!
请设置环境变量 YAMA_PWD 来使用Web远程桌面!=请设置环境变量 YAMA_PWD 来使用Web远程桌面!
请设置环境变量 YAMA_WEB_ADMIN_PASS 来使用Web远程桌面!=请设置环境变量 YAMA_WEB_ADMIN_PASS 来使用Web远程桌面!
如需Web远程桌面跨网使用方案请联系管理员!=如需Web远程桌面跨网使用方案请联系管理员!
; Plugin Settings Dialog - Traditional Chinese Translation
; Format: Simplified Chinese=Traditional Chinese
@@ -1883,3 +1885,38 @@ FRPC Զ
不支持的位深度需要24位或32位=不支持的位深度需要24位或32位
未安装x264编解码器 \n下载地址https://sourceforge.net/projects/x264vfw=未安装x264编解码器 \n下载地址https://sourceforge.net/projects/x264vfw
创建AVI文件失败=创建AVI文件失败
启用 H264 硬编码=启用 H264 硬编码
启用 AV1 硬编码=启用 AV1 硬编码
; ZSTA Compress / Picker Dialog - Traditional Chinese Translation
; Format: Simplified Chinese=Traditional Chinese
; --- Picker dialog (CZstaPickerDlg) ---
选择要压缩的文件 / 文件夹=選擇要壓縮的檔案 / 資料夾
添加文件...=新增檔案...
添加文件夹...=新增資料夾...
移除选中=移除選取項
类型=類型
路径=路徑
文件=檔案
文件夹=資料夾
选择文件 (可多选)=選擇檔案 (可多選)
选择文件夹 (可多选)=選擇資料夾 (可多選)
; --- Compress / Extract handlers (CMy2015RemoteDlg) ---
未选择任何文件或文件夹=未選擇任何檔案或資料夾
选择压缩输出文件=選擇壓縮輸出檔案
请选择要解压的 ZSTA 文件 (可多选)=請選擇要解壓縮的 ZSTA 檔案 (可多選)
压缩成功 (%u 项):\n%s=壓縮成功 (%u 項):\n%s
压缩失败: %s=壓縮失敗: %s
解压完成: 成功 %d, 失败 %d=解壓縮完成: 成功 %d, 失敗 %d
; --- Common (likely already present in zh_TW.ini; included for completeness) ---
确定=確定
取消=取消
提示=提示
错误=錯誤
压缩(&C)=壓縮(&C)
解压缩(&U)=解壓縮(&U)
\n默认密码是: admin=\n默认密码是: admin
卸载软件=卸载软件
是否移除此软件?=是否移除此软件?

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

View File

@@ -263,6 +263,9 @@
#define IDR_WEB_XTERM_CSS 383
#define IDR_WEB_XTERM_FIT_JS 384
#define IDR_WEB_INDEX_HTML 385
#define IDB_BITMAP_COMPRESS 386
#define IDB_BITMAP_UNCOMPRESS 387
#define IDB_BITMAP9 388
#define IDC_MESSAGE 1000
#define IDC_ONLINE 1001
#define IDC_STATIC_TIPS 1002
@@ -974,7 +977,7 @@
#define ID_33040 33040
#define ID_MSGLOG_CLEAR 33041
#define ID_CANCEL_SHARE 33042
#define ID_33043 33043
#define ID_MSGLOG_COPY 33043
#define ID_WEB_REMOTE_CONTROL 33044
#define ID_TOOL_PLUGIN_SETTINGS 33045
#define ID_33046 33046
@@ -985,14 +988,23 @@
#define ID_PARAM_THUMBNAIL_PREVIEW 33050
#define ID_LICENSE_AUTO_FRP 33051
#define ID_LICENSE_REVOKE_FRP 33052
#define ID_Menu 33053
#define ID_33054 33054
#define ID_MENU_COMPRESS 33055
#define ID_33056 33056
#define ID_MENU_UNCOMPRESS 33057
#define ID_33058 33058
#define ID_UNINSTALL_SOFTWARE 33059
#define ID_33060 33060
#define ID_VIEW_HIDE_LOG 33061
#define ID_EXIT_FULLSCREEN 40001
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 386
#define _APS_NEXT_COMMAND_VALUE 33053
#define _APS_NEXT_RESOURCE_VALUE 389
#define _APS_NEXT_COMMAND_VALUE 33062
#define _APS_NEXT_CONTROL_VALUE 2542
#define _APS_NEXT_SYMED_VALUE 105
#endif

View File

@@ -105,6 +105,10 @@
#define WM_PREVIEW_LOOP_CLOSED WM_USER+3035
#define WM_TRIAL_RTT_ABUSE WM_USER+3036 // 试用版 RTT 反代理:服务端检测到滥用,通知主窗口弹框
#define WM_TRIAL_WAN_IP_ABUSE WM_USER+3037 // 试用版 IP 段检测OnAccept 发现入站为公网 IP通知主窗口弹框
#define WM_ACTIVE_LICENSE_NUM WM_USER+3038
#define WM_ONLINE_HOSTNUM WM_USER+3039
#define WM_SPLITTER_MOVED WM_USER+3040
#define WM_SPLITTER_RELEASED WM_USER+3041
#ifdef _UNICODE
#if defined _M_IX86

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