mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-10 14:31:02 +03:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e17efa9474 | ||
|
|
8eda19247c | ||
|
|
1ac684db0d | ||
|
|
1c2dd71891 | ||
|
|
5b4d6baf47 | ||
|
|
7696b0ee51 | ||
|
|
20ab5ab0ad | ||
|
|
c01300be20 | ||
|
|
5f015c9da1 | ||
|
|
082a5a2a4e | ||
|
|
61f0944990 | ||
|
|
96e2a330b8 | ||
|
|
5abf4e9724 | ||
|
|
cf2b28faf9 | ||
|
|
bdb38c4730 | ||
|
|
fa418cace6 | ||
|
|
94a2a2bb4a | ||
|
|
865fe71c46 | ||
|
|
137298e05a | ||
|
|
685a89a171 | ||
|
|
12b5cc7f72 | ||
|
|
480e9e8234 | ||
|
|
29e1852a68 | ||
|
|
8314335b31 | ||
|
|
acb9f63e1d | ||
|
|
005a8b4a04 | ||
|
|
e2149974cc |
85
.github/workflows/wf-cliprdr-ci.yml
vendored
85
.github/workflows/wf-cliprdr-ci.yml
vendored
@@ -1,85 +0,0 @@
|
||||
name: wf-cliprdr CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- "libs/clipboard/src/windows/**"
|
||||
- "tests/test_invariant_wf_cliprdr.c"
|
||||
- ".github/workflows/wf-cliprdr-ci.yml"
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "libs/clipboard/src/windows/**"
|
||||
- "tests/test_invariant_wf_cliprdr.c"
|
||||
- ".github/workflows/wf-cliprdr-ci.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: wf_cliprdr invariant test
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up MSVC
|
||||
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
|
||||
with:
|
||||
arch: x64
|
||||
|
||||
- name: Setup vcpkg with GitHub Actions binary cache
|
||||
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||
with:
|
||||
vcpkgDirectory: C:\vcpkg
|
||||
doNotCache: false
|
||||
|
||||
- name: Install vcpkg dependency
|
||||
shell: pwsh
|
||||
run: |
|
||||
& "$env:VCPKG_ROOT\vcpkg.exe" install check:x64-windows --classic --x-install-root="$env:VCPKG_ROOT\installed"
|
||||
|
||||
- name: Build test
|
||||
shell: pwsh
|
||||
run: |
|
||||
$testRoot = Join-Path $env:GITHUB_WORKSPACE 'build\wf-cliprdr'
|
||||
New-Item -ItemType Directory -Force $testRoot | Out-Null
|
||||
|
||||
$testSource = (($env:GITHUB_WORKSPACE -replace '\\', '/') + '/tests/test_invariant_wf_cliprdr.c')
|
||||
$cmakeLists = @(
|
||||
'cmake_minimum_required(VERSION 3.20)'
|
||||
'project(test_invariant_wf_cliprdr C)'
|
||||
''
|
||||
'set(CMAKE_C_STANDARD 11)'
|
||||
'set(CMAKE_C_STANDARD_REQUIRED ON)'
|
||||
'set(CMAKE_C_EXTENSIONS OFF)'
|
||||
''
|
||||
'find_package(check CONFIG REQUIRED)'
|
||||
''
|
||||
'add_executable(test_invariant_wf_cliprdr'
|
||||
' "TEST_SOURCE"'
|
||||
')'
|
||||
''
|
||||
'target_link_libraries(test_invariant_wf_cliprdr PRIVATE'
|
||||
' $<$<TARGET_EXISTS:Check::check>:Check::check>'
|
||||
' $<$<NOT:$<TARGET_EXISTS:Check::check>>:Check::checkShared>'
|
||||
')'
|
||||
) -join [Environment]::NewLine
|
||||
$cmakeLists.Replace('TEST_SOURCE', $testSource) | Set-Content -NoNewline (Join-Path $testRoot 'CMakeLists.txt')
|
||||
|
||||
cmake -S $testRoot -B (Join-Path $testRoot 'out') -G "Visual Studio 17 2022" -A x64 -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows
|
||||
cmake --build (Join-Path $testRoot 'out') --config Release
|
||||
|
||||
- name: Run test
|
||||
shell: pwsh
|
||||
run: .\build\wf-cliprdr\out\Release\test_invariant_wf_cliprdr.exe
|
||||
32
Cargo.lock
generated
32
Cargo.lock
generated
@@ -771,6 +771,26 @@ dependencies = [
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.12.1",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2 1.0.93",
|
||||
"quote 1.0.36",
|
||||
"regex",
|
||||
"rustc-hash 2.1.1",
|
||||
"shlex",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.2"
|
||||
@@ -2329,7 +2349,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||
dependencies = [
|
||||
"libloading 0.7.4",
|
||||
"libloading 0.8.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2694,7 +2714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4494,7 +4514,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7434,7 +7454,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.11.0",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7491,7 +7511,7 @@ dependencies = [
|
||||
"security-framework 3.5.1",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7578,7 +7598,7 @@ name = "scrap"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"bindgen 0.65.1",
|
||||
"bindgen 0.72.1",
|
||||
"block",
|
||||
"cfg-if 1.0.0",
|
||||
"dbus",
|
||||
|
||||
@@ -6,83 +6,82 @@ ANDROID_ABI=$1
|
||||
|
||||
# Build RustDesk dependencies for Android using vcpkg.json
|
||||
# Required:
|
||||
# 1. set VCPKG_ROOT / ANDROID_NDK path environment variables
|
||||
# 1. set VCPKG_ROOT / ANDROID_NDK_HOME path environment variables
|
||||
# 2. vcpkg initialized
|
||||
# 3. ndk, version: r25c or newer
|
||||
|
||||
if [ -z "$ANDROID_NDK_HOME" ]; then
|
||||
echo "Failed! Please set ANDROID_NDK_HOME"
|
||||
exit 1
|
||||
if [ -z "${ANDROID_NDK_HOME}" ]; then
|
||||
echo "ERROR: Please set ANDROID_NDK_HOME environment variable" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$VCPKG_ROOT" ]; then
|
||||
echo "Failed! Please set VCPKG_ROOT"
|
||||
exit 1
|
||||
if [ -z "${VCPKG_ROOT}" ]; then
|
||||
echo "ERROR: Please set VCPKG_ROOT environment variable" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
API_LEVEL="21"
|
||||
case "${ANDROID_ABI}" in
|
||||
arm64-v8a)
|
||||
VCPKG_TARGET=arm64-android
|
||||
;;
|
||||
armeabi-v7a)
|
||||
VCPKG_TARGET=arm-neon-android
|
||||
;;
|
||||
x86_64)
|
||||
VCPKG_TARGET=x64-android
|
||||
;;
|
||||
x86)
|
||||
VCPKG_TARGET=x86-android
|
||||
;;
|
||||
*)
|
||||
echo "Usage: build_android_deps.sh <arm64-v8a|armeabi-v7a|x86_64|x86>" 1>&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Get directory of this script
|
||||
|
||||
SCRIPTDIR="$(readlink -f "$0")"
|
||||
SCRIPTDIR="$(dirname "$SCRIPTDIR")"
|
||||
SCRIPTDIR="$(dirname "${SCRIPTDIR}")"
|
||||
|
||||
# Check if vcpkg.json is one level up - in root directory of RD
|
||||
|
||||
if [ ! -f "$SCRIPTDIR/../vcpkg.json" ]; then
|
||||
echo "Failed! Please check where vcpkg.json is!"
|
||||
exit 1
|
||||
if [ ! -f "${SCRIPTDIR}/../vcpkg.json" ]; then
|
||||
echo "ERROR: Can not find vcpkg.json in RustDesk top-level directory" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# NDK llvm toolchain
|
||||
echo "INFO: Building and install vcpkg dependencies for Android ${ANDROID_ABI} ..."
|
||||
|
||||
HOST_TAG="linux-x86_64" # current platform, set as `ls $ANDROID_NDK/toolchains/llvm/prebuilt/`
|
||||
TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG
|
||||
pushd "${SCRIPTDIR}/.."
|
||||
|
||||
function build {
|
||||
ANDROID_ABI=$1
|
||||
"${VCPKG_ROOT}/vcpkg" install \
|
||||
--triplet "${VCPKG_TARGET}" \
|
||||
--x-install-root="${VCPKG_ROOT}/installed"
|
||||
|
||||
case "$ANDROID_ABI" in
|
||||
arm64-v8a)
|
||||
ABI=aarch64-linux-android$API_LEVEL
|
||||
VCPKG_TARGET=arm64-android
|
||||
;;
|
||||
armeabi-v7a)
|
||||
ABI=armv7a-linux-androideabi$API_LEVEL
|
||||
VCPKG_TARGET=arm-neon-android
|
||||
;;
|
||||
x86_64)
|
||||
ABI=x86_64-linux-android$API_LEVEL
|
||||
VCPKG_TARGET=x64-android
|
||||
;;
|
||||
x86)
|
||||
ABI=i686-linux-android$API_LEVEL
|
||||
VCPKG_TARGET=x86-android
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: ANDROID_ABI must be one of: arm64-v8a, armeabi-v7a, x86_64, x86" >&2
|
||||
return 1
|
||||
esac
|
||||
popd
|
||||
|
||||
echo "*** [$ANDROID_ABI][Start] Build and install vcpkg dependencies"
|
||||
pushd "$SCRIPTDIR/.."
|
||||
$VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed"
|
||||
popd
|
||||
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-$VCPKG_TARGET-rel-out.log" || true
|
||||
echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies"
|
||||
echo "INFO: Completed building vcpkg dependencies for Android ${ANDROID_ABI}"
|
||||
|
||||
if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then
|
||||
echo "*** [Start] Move arm-neon-android to arm-android"
|
||||
if [ "${ANDROID_ABI}" = 'armeabi-v7a' ]; then
|
||||
# Symlink arm-neon-android to arm-android because cargo-ndk does not
|
||||
# understand NEON suffix.
|
||||
|
||||
mv "$VCPKG_ROOT/installed/arm-neon-android" "$VCPKG_ROOT/installed/arm-android"
|
||||
if [ -d "${VCPKG_ROOT}/installed/arm-neon-android" ]; then
|
||||
echo 'INFO: Symlinking arm-neon-android to arm-android'
|
||||
|
||||
echo "*** [Finished] Move arm-neon-android to arm-android"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ ! -z "$ANDROID_ABI" ]; then
|
||||
build "$ANDROID_ABI"
|
||||
else
|
||||
echo "Usage: build-android-deps.sh <ANDROID-ABI>" >&2
|
||||
exit 1
|
||||
ln -sf \
|
||||
"${VCPKG_ROOT}/installed/arm-neon-android" \
|
||||
"${VCPKG_ROOT}/installed/arm-android"
|
||||
|
||||
echo 'INFO: Symlinked arm-neon-android to arm-android'
|
||||
else
|
||||
cat 0<<.a
|
||||
ERROR: 'vcpkg install' seem to complete successfully but
|
||||
directory '${VCPKG_ROOT}/installed/arm-neon-android' is missing!
|
||||
|
||||
.a
|
||||
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -495,14 +495,14 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
if (client.type_() == ClientType.file)
|
||||
FittedBox(
|
||||
child: Text(
|
||||
translate("File Transfer"),
|
||||
translate("Transfer file"),
|
||||
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
),
|
||||
if (client.type_() == ClientType.camera)
|
||||
FittedBox(
|
||||
child: Text(
|
||||
translate("View Camera"),
|
||||
translate("View camera"),
|
||||
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
typedef char** (*FUNC_RUSTDESK_CORE_MAIN)(int*);
|
||||
typedef void (*FUNC_RUSTDESK_FREE_ARGS)( char**, int);
|
||||
typedef int (*FUNC_RUSTDESK_GET_APP_NAME)(wchar_t*, int);
|
||||
typedef int (*FUNC_RUSTDESK_IS_DISABLE_INSTALLATION)();
|
||||
/// Note: `--server`, `--service` are already handled in [core_main.rs].
|
||||
const std::vector<std::string> parameters_white_list = {"--install", "--cm"};
|
||||
|
||||
@@ -62,6 +63,22 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
}
|
||||
std::vector<std::string> rust_args(c_args, c_args + args_len);
|
||||
free_c_args(c_args, args_len);
|
||||
FUNC_RUSTDESK_IS_DISABLE_INSTALLATION rustdesk_is_disable_installation =
|
||||
(FUNC_RUSTDESK_IS_DISABLE_INSTALLATION)GetProcAddress(hInstance, "rustdesk_is_disable_installation");
|
||||
bool is_disable_installation =
|
||||
rustdesk_is_disable_installation && rustdesk_is_disable_installation() != 0;
|
||||
const auto installParam = std::string("--install");
|
||||
// Flutter reads the original process command line, not only rust_args, so
|
||||
// remove the `--install` injected by the portable wrapper here as well. This
|
||||
// also lets `no-install.exe` continue as a portable app when installation is
|
||||
// disabled. See: https://github.com/rustdesk/rustdesk-server-pro/issues/991#issuecomment-4978376890
|
||||
if (is_disable_installation) {
|
||||
command_line_arguments.erase(
|
||||
std::remove(command_line_arguments.begin(),
|
||||
command_line_arguments.end(),
|
||||
installParam),
|
||||
command_line_arguments.end());
|
||||
}
|
||||
|
||||
std::wstring app_name = L"RustDesk";
|
||||
FUNC_RUSTDESK_GET_APP_NAME get_rustdesk_app_name = (FUNC_RUSTDESK_GET_APP_NAME)GetProcAddress(hInstance, "get_rustdesk_app_name");
|
||||
@@ -118,7 +135,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
is_cm_page = true;
|
||||
}
|
||||
bool is_install_page = false;
|
||||
auto installParam = std::string("--install");
|
||||
if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, installParam.size(), installParam.c_str()) == 0) {
|
||||
is_install_page = true;
|
||||
}
|
||||
|
||||
@@ -232,6 +232,7 @@ struct _CliprdrStream
|
||||
FILEDESCRIPTORW m_Dsc;
|
||||
void *m_pData;
|
||||
UINT32 m_connID;
|
||||
UINT32 m_streamId; // unique CLIPRDR streamId; avoids leaking a heap pointer
|
||||
};
|
||||
typedef struct _CliprdrStream CliprdrStream;
|
||||
|
||||
@@ -285,6 +286,9 @@ struct wf_clipboard
|
||||
char *req_fdata;
|
||||
HANDLE req_fevent;
|
||||
BOOL req_f_received;
|
||||
UINT32 req_f_conn_id_expected; // connID of the outstanding request
|
||||
UINT32 req_f_stream_id_expected; // streamId of the outstanding request; responses for another are dropped
|
||||
LONG req_f_stream_id_seq; // source of unique per-stream ids
|
||||
|
||||
size_t nFiles;
|
||||
size_t file_array_size;
|
||||
@@ -315,7 +319,7 @@ static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format);
|
||||
static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UINT32 format);
|
||||
static UINT cliprdr_send_lock(wfClipboard *clipboard);
|
||||
static UINT cliprdr_send_unlock(wfClipboard *clipboard);
|
||||
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid,
|
||||
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId,
|
||||
ULONG index, UINT32 flag, DWORD positionhigh,
|
||||
DWORD positionlow, ULONG request);
|
||||
|
||||
@@ -398,7 +402,7 @@ static ULONG STDMETHODCALLTYPE CliprdrStream_Release(IStream *This)
|
||||
static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULONG cb,
|
||||
ULONG *pcbRead)
|
||||
{
|
||||
int ret;
|
||||
UINT ret;
|
||||
CliprdrStream *instance = (CliprdrStream *)This;
|
||||
wfClipboard *clipboard;
|
||||
|
||||
@@ -411,12 +415,23 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO
|
||||
if (instance->m_lOffset.QuadPart >= instance->m_lSize.QuadPart)
|
||||
return S_FALSE;
|
||||
|
||||
ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)This, instance->m_lIndex,
|
||||
ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId, instance->m_lIndex,
|
||||
FILECONTENTS_RANGE, instance->m_lOffset.HighPart,
|
||||
instance->m_lOffset.LowPart, cb);
|
||||
|
||||
if (ret < 0)
|
||||
if (ret != CHANNEL_RC_OK)
|
||||
{
|
||||
free(clipboard->req_fdata);
|
||||
clipboard->req_fdata = NULL;
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
if (clipboard->req_fsize > cb)
|
||||
{
|
||||
free(clipboard->req_fdata);
|
||||
clipboard->req_fdata = NULL;
|
||||
return STG_E_READFAULT;
|
||||
}
|
||||
|
||||
if (clipboard->req_fdata)
|
||||
{
|
||||
@@ -628,6 +643,7 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
|
||||
instance->m_pData = pData;
|
||||
instance->m_lOffset.QuadPart = 0;
|
||||
instance->m_connID = connID;
|
||||
instance->m_streamId = (UINT32)InterlockedIncrement(&clipboard->req_f_stream_id_seq);
|
||||
|
||||
if (instance->m_Dsc.dwFlags & FD_ATTRIBUTES)
|
||||
{
|
||||
@@ -638,16 +654,28 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
|
||||
if (((instance->m_Dsc.dwFlags & FD_FILESIZE) == 0) && !isDir)
|
||||
{
|
||||
/* get content size of this stream */
|
||||
if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)instance,
|
||||
if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId,
|
||||
instance->m_lIndex, FILECONTENTS_SIZE, 0, 0,
|
||||
8) == CHANNEL_RC_OK)
|
||||
{
|
||||
success = TRUE;
|
||||
}
|
||||
|
||||
if (clipboard->req_fdata != NULL)
|
||||
if (clipboard->req_fdata != NULL && clipboard->req_fsize >= sizeof(LONGLONG))
|
||||
{
|
||||
LONGLONG sz = 0;
|
||||
CopyMemory(&sz, clipboard->req_fdata, sizeof(sz));
|
||||
if (sz < 0)
|
||||
success = FALSE;
|
||||
else
|
||||
instance->m_lSize.QuadPart = sz;
|
||||
}
|
||||
else
|
||||
{
|
||||
success = FALSE;
|
||||
}
|
||||
if (clipboard->req_fdata)
|
||||
{
|
||||
instance->m_lSize.QuadPart = *((LONGLONG *)clipboard->req_fdata);
|
||||
free(clipboard->req_fdata);
|
||||
clipboard->req_fdata = NULL;
|
||||
}
|
||||
@@ -1773,12 +1801,12 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
|
||||
return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem);
|
||||
}
|
||||
|
||||
UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index,
|
||||
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId, ULONG index,
|
||||
UINT32 flag, DWORD positionhigh, DWORD positionlow,
|
||||
ULONG nreq)
|
||||
{
|
||||
UINT rc;
|
||||
CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest;
|
||||
CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest = { 0 };
|
||||
|
||||
if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
@@ -1789,12 +1817,11 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
|
||||
return rc;
|
||||
}
|
||||
clipboard->req_f_received = FALSE;
|
||||
clipboard->req_f_conn_id_expected = connID;
|
||||
clipboard->req_f_stream_id_expected = streamId;
|
||||
|
||||
fileContentsRequest.connID = connID;
|
||||
// streamId is `IStream*` pointer, though it is not very good on a 64-bit system.
|
||||
// But it is OK, because it is only used to check if the stream is the same in
|
||||
// `wf_cliprdr_server_file_contents_request()` function.
|
||||
fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid;
|
||||
fileContentsRequest.streamId = streamId;
|
||||
fileContentsRequest.listIndex = index;
|
||||
fileContentsRequest.dwFlags = flag;
|
||||
fileContentsRequest.nPositionLow = positionlow;
|
||||
@@ -3015,6 +3042,7 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
||||
BOOL bIsStreamFile = TRUE;
|
||||
static LPSTREAM pStreamStc = NULL;
|
||||
static UINT32 uStreamIdStc = 0;
|
||||
static UINT32 uConnIdStc = 0;
|
||||
wfClipboard *clipboard;
|
||||
UINT rc = ERROR_INTERNAL_ERROR;
|
||||
UINT sRc;
|
||||
@@ -3088,7 +3116,8 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
||||
vFormatEtc.lindex = fileContentsRequest->listIndex;
|
||||
vFormatEtc.ptd = NULL;
|
||||
|
||||
if ((uStreamIdStc != fileContentsRequest->streamId) || !pStreamStc)
|
||||
if ((uStreamIdStc != fileContentsRequest->streamId) ||
|
||||
(uConnIdStc != fileContentsRequest->connID) || !pStreamStc)
|
||||
{
|
||||
LPENUMFORMATETC pEnumFormatEtc;
|
||||
ULONG CeltFetched;
|
||||
@@ -3119,6 +3148,7 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
||||
{
|
||||
pStreamStc = vStgMedium.pstm;
|
||||
uStreamIdStc = fileContentsRequest->streamId;
|
||||
uConnIdStc = fileContentsRequest->connID;
|
||||
bIsStreamFile = TRUE;
|
||||
}
|
||||
|
||||
@@ -3282,6 +3312,9 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
if (fileContentsResponse->connID != clipboard->req_f_conn_id_expected ||
|
||||
fileContentsResponse->streamId != clipboard->req_f_stream_id_expected)
|
||||
return CHANNEL_RC_OK;
|
||||
clipboard->req_fsize = 0;
|
||||
clipboard->req_fdata = NULL;
|
||||
|
||||
@@ -3292,6 +3325,13 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
|
||||
}
|
||||
|
||||
clipboard->req_fsize = fileContentsResponse->cbRequested;
|
||||
/*
|
||||
* Keep the zero-size allocation: supported Windows builds use the Microsoft
|
||||
* CRT, where malloc(0) returns a valid pointer. wait_response_event() also
|
||||
* uses a non-NULL req_fdata to recognize a successful zero-byte response.
|
||||
* The Rust FFI derives requestedData and cbRequested from the same Vec, so a
|
||||
* nonzero length cannot have a NULL data pointer on the normal call path.
|
||||
*/
|
||||
clipboard->req_fdata = (char *)malloc(fileContentsResponse->cbRequested);
|
||||
if (!clipboard->req_fdata)
|
||||
{
|
||||
|
||||
@@ -48,7 +48,7 @@ quest = "0.3"
|
||||
|
||||
[build-dependencies]
|
||||
target_build_utils = "0.3"
|
||||
bindgen = "0.65"
|
||||
bindgen = "0.72.1"
|
||||
pkg-config = { version = "0.3.27", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
|
||||
@@ -4,19 +4,48 @@ use std::{
|
||||
println,
|
||||
};
|
||||
|
||||
/// The static library name may differ from the package name.
|
||||
fn link_lib_name(package: &str) -> &str {
|
||||
match package {
|
||||
"svt-av1" => "SvtAv1Enc",
|
||||
_ => package.trim_start_matches("lib"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "linux-pkg-config"))]
|
||||
fn link_pkg_config(name: &str) -> Vec<PathBuf> {
|
||||
// sometimes an override is needed
|
||||
let pc_name = match name {
|
||||
"libvpx" => "vpx",
|
||||
"svt-av1" => "SvtAv1Enc",
|
||||
_ => name,
|
||||
};
|
||||
let lib = pkg_config::probe_library(pc_name)
|
||||
let mut pc = pkg_config::Config::new();
|
||||
if name == "svt-av1" {
|
||||
// svt_av1.rs requires the 4.x API (rtc, PredStructure, on-the-fly rate events)
|
||||
pc.atleast_version("4.2");
|
||||
}
|
||||
let pkg_hint = match name {
|
||||
"svt-av1" => "libsvt-av1-dev (needs svt-av1 >= 4.2)".to_owned(),
|
||||
_ => format!("{pc_name}-dev"),
|
||||
};
|
||||
let lib = pc.probe(pc_name)
|
||||
.expect(format!(
|
||||
"unable to find '{pc_name}' development headers with pkg-config (feature linux-pkg-config is enabled).
|
||||
try installing '{pc_name}-dev' from your system package manager.").as_str());
|
||||
try installing '{pkg_hint}' from your system package manager.").as_str());
|
||||
|
||||
lib.include_paths
|
||||
let mut include_paths = lib.include_paths;
|
||||
// SvtAv1Enc.pc's Cflags is -I${includedir}/svt-av1, but svt_av1_ffi.h uses
|
||||
// #include <svt-av1/...>, so the parent include root is needed too.
|
||||
if name == "svt-av1" {
|
||||
let parents: Vec<PathBuf> = include_paths
|
||||
.iter()
|
||||
.filter(|p| p.ends_with("svt-av1"))
|
||||
.filter_map(|p| p.parent().map(|p| p.to_path_buf()))
|
||||
.collect();
|
||||
include_paths.extend(parents);
|
||||
}
|
||||
include_paths
|
||||
}
|
||||
#[cfg(not(all(target_os = "linux", feature = "linux-pkg-config")))]
|
||||
fn link_pkg_config(_name: &str) -> Vec<PathBuf> {
|
||||
@@ -61,10 +90,7 @@ fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf {
|
||||
path.push("installed");
|
||||
}
|
||||
path.push(target);
|
||||
println!(
|
||||
"cargo:rustc-link-lib=static={}",
|
||||
name.trim_start_matches("lib")
|
||||
);
|
||||
println!("cargo:rustc-link-lib=static={}", link_lib_name(name));
|
||||
println!(
|
||||
"cargo:rustc-link-search={}",
|
||||
path.join("lib").to_str().unwrap()
|
||||
@@ -103,11 +129,13 @@ fn link_homebrew_m1(name: &str) -> PathBuf {
|
||||
);
|
||||
}
|
||||
path.push(directories.pop().unwrap());
|
||||
// Link the library.
|
||||
println!(
|
||||
"cargo:rustc-link-lib=static={}",
|
||||
name.trim_start_matches("lib")
|
||||
);
|
||||
// Link the library. Homebrew's svt-av1 bottle only ships a dylib.
|
||||
if name == "svt-av1" {
|
||||
println!("cargo:rustc-link-lib={}", link_lib_name(name));
|
||||
println!("cargo:warning=svt-av1 is linked dynamically from homebrew, the binary needs homebrew's svt-av1 at runtime and is not relocatable; set VCPKG_ROOT for a static build");
|
||||
} else {
|
||||
println!("cargo:rustc-link-lib=static={}", link_lib_name(name));
|
||||
}
|
||||
// Add the library path.
|
||||
println!(
|
||||
"cargo:rustc-link-search={}",
|
||||
@@ -120,8 +148,9 @@ fn link_homebrew_m1(name: &str) -> PathBuf {
|
||||
}
|
||||
|
||||
/// Find package. By default, it will try to find vcpkg first, then homebrew(currently only for Mac M1).
|
||||
/// If building for linux and feature "linux-pkg-config" is enabled, will try to use pkg-config
|
||||
/// unless check fails (e.g. NO_PKG_CONFIG_libyuv=1)
|
||||
/// If building on a linux host and feature "linux-pkg-config" is enabled, will try to use pkg-config
|
||||
/// unless check fails (e.g. NO_PKG_CONFIG_libyuv=1). Note the cfg! below sees the build host, not
|
||||
/// the target, so cross builds (e.g. linux -> android) must not enable linux-pkg-config.
|
||||
fn find_package(name: &str) -> Vec<PathBuf> {
|
||||
let no_pkg_config_var_name = format!("NO_PKG_CONFIG_{name}");
|
||||
println!("cargo:rerun-if-env-changed={no_pkg_config_var_name}");
|
||||
@@ -170,9 +199,9 @@ fn gen_vcpkg_package(package: &str, ffi_header: &str, generated: &str, regex: &s
|
||||
let out_dir = Path::new(&out_dir);
|
||||
|
||||
let ffi_header = src_dir.join("src").join("bindings").join(ffi_header);
|
||||
println!("rerun-if-changed={}", ffi_header.display());
|
||||
println!("cargo:rerun-if-changed={}", ffi_header.display());
|
||||
for dir in &includes {
|
||||
println!("rerun-if-changed={}", dir.display());
|
||||
println!("cargo:rerun-if-changed={}", dir.display());
|
||||
}
|
||||
|
||||
let ffi_rs = out_dir.join(generated);
|
||||
@@ -248,6 +277,18 @@ fn main() {
|
||||
gen_vcpkg_package("libvpx", "vpx_ffi.h", "vpx_ffi.rs", "^[vV].*");
|
||||
gen_vcpkg_package("aom", "aom_ffi.h", "aom_ffi.rs", "^(aom|AOM|OBU|AV1).*");
|
||||
gen_vcpkg_package("libyuv", "yuv_ffi.h", "yuv_ffi.rs", ".*");
|
||||
// Skipped on 32-bit targets: svt-av1 does not support them, the vcpkg
|
||||
// manifest does not install it there and disable_av1() never uses AV1 on
|
||||
// them anyway. Must stay in sync with the cfg gates in
|
||||
// mod.rs/codec.rs/video_service.rs.
|
||||
if env::var("CARGO_CFG_TARGET_POINTER_WIDTH").as_deref() == Ok("64") {
|
||||
gen_vcpkg_package(
|
||||
"svt-av1",
|
||||
"svt_av1_ffi.h",
|
||||
"svt_av1_ffi.rs",
|
||||
"^(svt_av1_|Svt|SVT_|Eb|EB_|PredStructure|PrivDataType).*",
|
||||
);
|
||||
}
|
||||
// ffmpeg();
|
||||
|
||||
if target_os == "ios" {
|
||||
|
||||
1096
libs/scrap/examples/av1_benchmark.rs
Normal file
1096
libs/scrap/examples/av1_benchmark.rs
Normal file
File diff suppressed because it is too large
Load Diff
5
libs/scrap/src/bindings/svt_av1_ffi.h
Normal file
5
libs/scrap/src/bindings/svt_av1_ffi.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#include <svt-av1/EbSvtAv1.h>
|
||||
#include <svt-av1/EbSvtAv1Enc.h>
|
||||
#include <svt-av1/EbSvtAv1ErrorCodes.h>
|
||||
#include <svt-av1/EbSvtAv1Formats.h>
|
||||
#include <svt-av1/EbSvtAv1Metadata.h>
|
||||
@@ -9,6 +9,8 @@ use std::{
|
||||
use crate::hwcodec::*;
|
||||
#[cfg(feature = "mediacodec")]
|
||||
use crate::mediacodec::{MediaCodecDecoder, H264_DECODER_SUPPORT, H265_DECODER_SUPPORT};
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
use crate::svt_av1::{SvtAv1Encoder, SvtAv1EncoderConfig};
|
||||
#[cfg(feature = "vram")]
|
||||
use crate::vram::*;
|
||||
use crate::{
|
||||
@@ -51,6 +53,8 @@ pub const ENCODE_NEED_SWITCH: &'static str = "ENCODE_NEED_SWITCH";
|
||||
pub enum EncoderCfg {
|
||||
VPX(VpxEncoderConfig),
|
||||
AOM(AomEncoderConfig),
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
SVTAV1(SvtAv1EncoderConfig),
|
||||
#[cfg(feature = "hwcodec")]
|
||||
HWRAM(HwRamEncoderConfig),
|
||||
#[cfg(feature = "vram")]
|
||||
@@ -71,6 +75,11 @@ pub trait EncoderApi {
|
||||
|
||||
fn set_quality(&mut self, ratio: f32) -> ResultType<()>;
|
||||
|
||||
/// Inform the encoder of the current pacing rate. Only rate controls that
|
||||
/// budget per frame from a configured frame rate need to override the
|
||||
/// default no-op.
|
||||
fn set_fps(&mut self, _fps: u32) {}
|
||||
|
||||
fn bitrate(&self) -> u32;
|
||||
|
||||
fn support_changing_quality(&self) -> bool;
|
||||
@@ -137,9 +146,34 @@ impl Encoder {
|
||||
EncoderCfg::VPX(_) => Ok(Encoder {
|
||||
codec: Box::new(VpxEncoder::new(config, i444)?),
|
||||
}),
|
||||
EncoderCfg::AOM(_) => Ok(Encoder {
|
||||
codec: Box::new(AomEncoder::new(config, i444)?),
|
||||
}),
|
||||
EncoderCfg::AOM(_) => {
|
||||
log::info!("AV1 encoder: aom");
|
||||
Ok(Encoder {
|
||||
codec: Box::new(AomEncoder::new(config, i444)?),
|
||||
})
|
||||
}
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
EncoderCfg::SVTAV1(svt) => match SvtAv1Encoder::new(EncoderCfg::SVTAV1(svt), i444) {
|
||||
Ok(codec) => {
|
||||
log::info!("AV1 encoder: svt-av1");
|
||||
Ok(Encoder {
|
||||
codec: Box::new(codec),
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
// Same wire format, aom keeps the negotiated AV1 session alive.
|
||||
log::error!("AV1 encoder: svt-av1 init failed ({e:?}), using aom");
|
||||
let aom = EncoderCfg::AOM(AomEncoderConfig {
|
||||
width: svt.width,
|
||||
height: svt.height,
|
||||
quality: svt.quality,
|
||||
keyframe_interval: svt.keyframe_interval,
|
||||
});
|
||||
Ok(Encoder {
|
||||
codec: Box::new(AomEncoder::new(aom, i444)?),
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
EncoderCfg::HWRAM(_) => match HwRamEncoder::new(config, i444) {
|
||||
@@ -269,7 +303,10 @@ impl Encoder {
|
||||
let preference = most_frequent.enum_value_or(PreferCodec::Auto);
|
||||
|
||||
// auto: h265 > h264 > av1/vp9/vp8
|
||||
let av1_test = Config::get_option(hbb_common::config::keys::OPTION_AV1_TEST) != "N";
|
||||
let av1_test = {
|
||||
let v = Config::get_option(hbb_common::config::keys::OPTION_AV1_TEST);
|
||||
v != "N" && v != "SVT-N"
|
||||
};
|
||||
let mut auto_codec = if av1_useable && av1_test {
|
||||
CodecFormat::AV1
|
||||
} else {
|
||||
@@ -365,6 +402,8 @@ impl Encoder {
|
||||
VpxVideoCodecId::VP9 => CodecFormat::VP9,
|
||||
},
|
||||
EncoderCfg::AOM(_) => CodecFormat::AV1,
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
EncoderCfg::SVTAV1(_) => CodecFormat::AV1,
|
||||
#[cfg(feature = "hwcodec")]
|
||||
EncoderCfg::HWRAM(hw) => {
|
||||
let name = hw.name.to_lowercase();
|
||||
@@ -411,6 +450,10 @@ impl Encoder {
|
||||
VpxVideoCodecId::VP9 => decodings.iter().all(|d| d.1.i444.vp9),
|
||||
},
|
||||
EncoderCfg::AOM(_) => decodings.iter().all(|d| d.1.i444.av1),
|
||||
// Mirrors AOM so an i444 preference change still triggers the encoder
|
||||
// rebuild, the rebuild then selects aom for the actual i444 encoding.
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
EncoderCfg::SVTAV1(_) => decodings.iter().all(|d| d.1.i444.av1),
|
||||
#[cfg(feature = "hwcodec")]
|
||||
EncoderCfg::HWRAM(_) => false,
|
||||
#[cfg(feature = "vram")]
|
||||
@@ -1042,7 +1085,15 @@ pub fn test_av1() {
|
||||
use hbb_common::rand::Rng;
|
||||
use std::{sync::Once, time::Duration};
|
||||
|
||||
if disable_av1() || !Config::get_option(OPTION_AV1_TEST).is_empty() {
|
||||
// The verdict is tied to the encoder generation that produced it, so
|
||||
// switching the AV1 encoder (aom <-> svt-av1) re-runs the test.
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
const AV1_TEST_RESULT: (&str, &str) = ("SVT-Y", "SVT-N");
|
||||
#[cfg(not(target_pointer_width = "64"))]
|
||||
const AV1_TEST_RESULT: (&str, &str) = ("Y", "N");
|
||||
|
||||
let cached = Config::get_option(OPTION_AV1_TEST);
|
||||
if disable_av1() || cached == AV1_TEST_RESULT.0 || cached == AV1_TEST_RESULT.1 {
|
||||
log::info!("skip test av1");
|
||||
return;
|
||||
}
|
||||
@@ -1101,15 +1152,40 @@ pub fn test_av1() {
|
||||
}
|
||||
Ok(dst)
|
||||
};
|
||||
let Ok(mut av1) = AomEncoder::new(
|
||||
EncoderCfg::AOM(AomEncoderConfig {
|
||||
let aom = || {
|
||||
AomEncoder::new(
|
||||
EncoderCfg::AOM(AomEncoderConfig {
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
keyframe_interval,
|
||||
}),
|
||||
i444,
|
||||
)
|
||||
.map(|e| Box::new(e) as Box<dyn EncoderApi>)
|
||||
};
|
||||
// Test the encoder actually used for AV1, svt-av1 on 64-bit targets, with
|
||||
// the same fallback to aom as Encoder::new so an svt init failure
|
||||
// does not brand a working aom as unusable. Note that i444 (true
|
||||
// color) sessions and svt-unsupported resolutions still run aom,
|
||||
// whose speed is then not covered by this gate.
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
let av1 = crate::svt_av1::SvtAv1Encoder::new(
|
||||
EncoderCfg::SVTAV1(crate::svt_av1::SvtAv1EncoderConfig {
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
keyframe_interval,
|
||||
qp_range: None,
|
||||
preset: None,
|
||||
}),
|
||||
i444,
|
||||
) else {
|
||||
)
|
||||
.map(|e| Box::new(e) as Box<dyn EncoderApi>)
|
||||
.or_else(|_| aom());
|
||||
#[cfg(not(target_pointer_width = "64"))]
|
||||
let av1 = aom();
|
||||
let Ok(mut av1) = av1 else {
|
||||
return false;
|
||||
};
|
||||
let mut key_frame_time = Duration::ZERO;
|
||||
@@ -1122,7 +1198,7 @@ pub fn test_av1() {
|
||||
};
|
||||
let start = Instant::now();
|
||||
if av1
|
||||
.encode(pts.elapsed().as_millis() as _, &yuv, super::STRIDE_ALIGN)
|
||||
.encode_to_message(EncodeInput::YUV(&yuv), pts.elapsed().as_millis() as _)
|
||||
.is_err()
|
||||
{
|
||||
log::debug!("av1 encode failed");
|
||||
@@ -1150,7 +1226,12 @@ pub fn test_av1() {
|
||||
let v = f();
|
||||
Config::set_option(
|
||||
OPTION_AV1_TEST.to_string(),
|
||||
if v { "Y" } else { "N" }.to_string(),
|
||||
if v {
|
||||
AV1_TEST_RESULT.0
|
||||
} else {
|
||||
AV1_TEST_RESULT.1
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,6 +52,8 @@ pub mod aom;
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
pub mod camera;
|
||||
pub mod record;
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
pub mod svt_av1;
|
||||
mod vpx;
|
||||
|
||||
#[repr(usize)]
|
||||
|
||||
536
libs/scrap/src/common/svt_av1.rs
Normal file
536
libs/scrap/src/common/svt_av1.rs
Normal file
@@ -0,0 +1,536 @@
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(improper_ctypes)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/svt_av1_ffi.rs"));
|
||||
|
||||
use crate::codec::{base_bitrate, codec_thread_num, EncoderApi, EncoderCfg};
|
||||
use crate::{EncodeInput, EncodeYuvFormat, Pixfmt, STRIDE_ALIGN};
|
||||
use hbb_common::{
|
||||
anyhow::Context,
|
||||
bail,
|
||||
bytes::Bytes,
|
||||
log,
|
||||
message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame},
|
||||
ResultType,
|
||||
};
|
||||
use std::{mem::MaybeUninit, os::raw::c_void, ptr, slice};
|
||||
|
||||
// SVT-AV1 rejects on-the-fly target bitrates above 100_000 kbps.
|
||||
const MAX_TARGET_BITRATE_KBPS: u32 = 100_000;
|
||||
// CBR budgets bits per frame from the configured frame rate, VideoQoS pushes
|
||||
// its authoritative pacing rate in via set_fps.
|
||||
const DEFAULT_FPS: u32 = 30;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SvtAv1EncoderConfig {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub quality: f32,
|
||||
pub keyframe_interval: Option<usize>,
|
||||
// SVT cannot update min/max QP on the fly. Product callers leave this
|
||||
// unset so dynamic quality changes retain the full project QP envelope;
|
||||
// controlled comparisons can pin the same range used by another encoder.
|
||||
pub qp_range: Option<(u32, u32)>,
|
||||
// RTC supports M7 through M13. Product callers use the screen-content
|
||||
// default; benchmarks can override it to measure the speed/quality curve.
|
||||
pub preset: Option<i8>,
|
||||
}
|
||||
|
||||
pub struct SvtAv1Encoder {
|
||||
handle: *mut EbComponentType,
|
||||
width: usize,
|
||||
height: usize,
|
||||
yuvfmt: EncodeYuvFormat,
|
||||
// current target bitrate in kbps, same unit as aom's rc_target_bitrate
|
||||
bitrate: u32,
|
||||
// bitrate change (kbps) to apply with the next picture via RATE_CHANGE_EVENT
|
||||
pending_bitrate: Option<u32>,
|
||||
// frame rate currently configured in the encoder
|
||||
fps: u32,
|
||||
// frame rate change to apply with the next picture via FRAME_RATE_CHANGE_EVENT
|
||||
pending_fps: Option<u32>,
|
||||
}
|
||||
|
||||
// The handle is only used behind &mut self, SVT-AV1 synchronizes internally.
|
||||
unsafe impl Send for SvtAv1Encoder {}
|
||||
|
||||
impl EncoderApi for SvtAv1Encoder {
|
||||
fn new(cfg: EncoderCfg, i444: bool) -> ResultType<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match cfg {
|
||||
EncoderCfg::SVTAV1(config) => {
|
||||
if i444 {
|
||||
// SVT-AV1 only supports 4:2:0 input, callers must fall back to aom for i444.
|
||||
bail!("svt-av1 encoder does not support I444");
|
||||
}
|
||||
if !Self::support(config.width, config.height) {
|
||||
bail!(
|
||||
"svt-av1 encoder does not support resolution {}x{}",
|
||||
config.width,
|
||||
config.height
|
||||
);
|
||||
}
|
||||
if let Some((min_qp, max_qp)) = config.qp_range {
|
||||
if min_qp > max_qp || max_qp > 63 {
|
||||
bail!("invalid svt-av1 QP range {min_qp}..{max_qp}");
|
||||
}
|
||||
}
|
||||
if let Some(preset) = config.preset {
|
||||
if !(7..=13).contains(&preset) {
|
||||
bail!("invalid svt-av1 RTC preset M{preset}; expected M7..M13");
|
||||
}
|
||||
}
|
||||
let mut handle: *mut EbComponentType = ptr::null_mut();
|
||||
let mut c: MaybeUninit<EbSvtAv1EncConfiguration> = MaybeUninit::zeroed();
|
||||
let res = unsafe { svt_av1_enc_init_handle(&mut handle, c.as_mut_ptr()) };
|
||||
if res != EbErrorType::EB_ErrorNone || handle.is_null() {
|
||||
bail!("svt_av1_enc_init_handle failed: {res:?}");
|
||||
}
|
||||
// c is loaded with the library defaults now, only override what we need.
|
||||
let mut c = unsafe { c.assume_init() };
|
||||
let bitrate = Self::bitrate(config.width, config.height, config.quality)
|
||||
.min(MAX_TARGET_BITRATE_KBPS);
|
||||
Self::apply_config(&mut c, &config, bitrate);
|
||||
let mut res = unsafe { svt_av1_enc_set_parameter(handle, &mut c) };
|
||||
if res == EbErrorType::EB_ErrorNone {
|
||||
res = unsafe { svt_av1_enc_init(handle) };
|
||||
}
|
||||
if res != EbErrorType::EB_ErrorNone {
|
||||
unsafe {
|
||||
svt_av1_enc_deinit_handle(handle);
|
||||
}
|
||||
bail!("failed to init svt-av1 encoder: {res:?}");
|
||||
}
|
||||
Ok(Self {
|
||||
handle,
|
||||
width: config.width as _,
|
||||
height: config.height as _,
|
||||
yuvfmt: Self::get_yuvfmt(config.width, config.height),
|
||||
bitrate,
|
||||
pending_bitrate: None,
|
||||
fps: DEFAULT_FPS,
|
||||
pending_fps: None,
|
||||
})
|
||||
}
|
||||
_ => bail!("encoder type mismatch"),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_to_message(&mut self, input: EncodeInput, ms: i64) -> ResultType<VideoFrame> {
|
||||
let frames = self
|
||||
.encode(ms, input.yuv()?)
|
||||
.with_context(|| "Failed to encode")?;
|
||||
if frames.len() > 0 {
|
||||
Ok(Self::create_video_frame(frames))
|
||||
} else {
|
||||
bail!("no valid frame");
|
||||
}
|
||||
}
|
||||
|
||||
fn yuvfmt(&self) -> EncodeYuvFormat {
|
||||
self.yuvfmt.clone()
|
||||
}
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
fn input_texture(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_quality(&mut self, ratio: f32) -> ResultType<()> {
|
||||
let bitrate =
|
||||
Self::bitrate(self.width as _, self.height as _, ratio).min(MAX_TARGET_BITRATE_KBPS);
|
||||
if bitrate > 0 && bitrate != self.bitrate {
|
||||
self.bitrate = bitrate;
|
||||
self.pending_bitrate = Some(bitrate);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_fps(&mut self, fps: u32) {
|
||||
if fps == 0 {
|
||||
return;
|
||||
}
|
||||
// also cancels a queued change that a later call made moot again
|
||||
if fps == self.fps {
|
||||
self.pending_fps = None;
|
||||
} else {
|
||||
self.pending_fps = Some(fps);
|
||||
}
|
||||
}
|
||||
|
||||
fn bitrate(&self) -> u32 {
|
||||
self.bitrate
|
||||
}
|
||||
|
||||
fn support_changing_quality(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn latency_free(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_hardware(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn disable(&self) {}
|
||||
}
|
||||
|
||||
impl SvtAv1Encoder {
|
||||
pub fn support(width: u32, height: u32) -> bool {
|
||||
width >= 64
|
||||
&& height >= 64
|
||||
&& width <= 16384
|
||||
&& height <= 8704
|
||||
&& width % 2 == 0
|
||||
&& height % 2 == 0
|
||||
}
|
||||
|
||||
fn apply_config(c: &mut EbSvtAv1EncConfiguration, cfg: &SvtAv1EncoderConfig, bitrate: u32) {
|
||||
c.enc_mode = cfg
|
||||
.preset
|
||||
.unwrap_or_else(|| Self::preset(cfg.width, cfg.height));
|
||||
c.source_width = cfg.width;
|
||||
c.source_height = cfg.height;
|
||||
// CBR budgets bits per frame from this rate, set_fps adjusts it on the
|
||||
// fly with FRAME_RATE_CHANGE_EVENT.
|
||||
c.frame_rate_numerator = DEFAULT_FPS;
|
||||
c.frame_rate_denominator = 1;
|
||||
c.encoder_bit_depth = 8;
|
||||
c.encoder_color_format = EbColorFormat::EB_YUV420;
|
||||
c.profile = EbAv1SeqProfile::MAIN_PROFILE;
|
||||
// Low delay + rtc + CBR: one packet out per picture in, svt_av1_enc_get_packet
|
||||
// blocks until the packet for the sent picture is ready, and rate/keyframe
|
||||
// changes on the fly are allowed.
|
||||
c.pred_structure = PredStructure::LOW_DELAY;
|
||||
// A four-picture temporal hierarchy improves reference efficiency without
|
||||
// introducing B-frame reordering. Two is also the highest hierarchy SVT
|
||||
// validates for low-delay CBR, and still preserves one packet per input.
|
||||
c.hierarchical_levels = 2;
|
||||
c.rtc = true;
|
||||
c.rate_control_mode = SvtAv1RcMode::SVT_AV1_RC_MODE_CBR as _;
|
||||
c.target_bit_rate = bitrate.min(MAX_TARGET_BITRATE_KBPS) * 1000;
|
||||
let initial_qp_range = Self::quality_qp_range(cfg.quality);
|
||||
let (min_qp, max_qp) = cfg.qp_range.unwrap_or((5, 45));
|
||||
c.min_qp_allowed = min_qp;
|
||||
c.max_qp_allowed = max_qp;
|
||||
c.qp = ((initial_qp_range.0 + initial_qp_range.1) / 2).clamp(min_qp, max_qp);
|
||||
c.look_ahead_distance = 0;
|
||||
// SVT 4.2's RTC CBR path only re-runs mode decision once when an inter
|
||||
// frame would overflow the virtual buffer. This keeps network bursts
|
||||
// bounded without enabling unrestricted multi-pass re-encoding.
|
||||
c.recode_loop = 1; // ALLOW_RECODE_KFMAXBW / one RTC VBV recode
|
||||
c.scene_change_detection = 0;
|
||||
// Temporal filtering is primarily useful for camera/video content. It
|
||||
// costs CPU and can soften text, while low-delay screen sharing cannot
|
||||
// benefit from future-frame filtering.
|
||||
c.enable_tf = 0;
|
||||
c.enable_tf_key = false;
|
||||
c.enable_overlays = false;
|
||||
// Use SVT's anti-alias-aware adaptive screen detection: UI/text can use
|
||||
// Palette and IntraBC while camera/video regions stay on the natural-
|
||||
// content path. RTC only supports these tools at M8 or slower.
|
||||
c.screen_content_mode = 3;
|
||||
c.enable_intrabc = true;
|
||||
c.tune = 1; // PSNR, low delay does not support tune 0
|
||||
c.level_of_parallelism = Self::parallelism();
|
||||
c.intra_refresh_type = SvtAv1IntraRefreshType::SVT_AV1_KF_REFRESH; // closed GOP
|
||||
c.intra_period_length = match cfg.keyframe_interval {
|
||||
Some(keyframe_interval) => keyframe_interval.saturating_sub(1) as _,
|
||||
None => -1, // no periodic intra refresh, keyframes only on demand
|
||||
};
|
||||
// pic_type is left to the encoder and keyframes come from encoder
|
||||
// restarts, the force_key_frames flag must stay off for low delay CBR
|
||||
// (the library resets it with a warning).
|
||||
c.force_key_frames = false;
|
||||
}
|
||||
|
||||
fn preset(_width: u32, _height: u32) -> i8 {
|
||||
// SVT-AV1 forces screen_content_mode to 0 for RTC presets M9 and faster,
|
||||
// so use M8 to keep the screen-content path enabled.
|
||||
// TODO: After benchmarking, select a quality-dependent preset in M0..=M8.
|
||||
8
|
||||
}
|
||||
|
||||
fn parallelism() -> u32 {
|
||||
// level_of_parallelism is a level from 1 to 6, not a thread count.
|
||||
match codec_thread_num(64) {
|
||||
n if n >= 32 => 6,
|
||||
n if n >= 16 => 5,
|
||||
n if n >= 8 => 4,
|
||||
n if n >= 4 => 3,
|
||||
n if n >= 2 => 2,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn encode(&mut self, ms: i64, data: &[u8]) -> ResultType<Vec<EncodedVideoFrame>> {
|
||||
let fmt = &self.yuvfmt;
|
||||
let chroma_height = (fmt.h + 1) / 2;
|
||||
let len = fmt.v + fmt.stride[2] * chroma_height;
|
||||
if data.len() < len {
|
||||
bail!("len not enough: {} < {}", data.len(), len);
|
||||
}
|
||||
let mut io = EbSvtIOFormat {
|
||||
luma: data.as_ptr() as *mut u8,
|
||||
cb: data[fmt.u..].as_ptr() as *mut u8,
|
||||
cr: data[fmt.v..].as_ptr() as *mut u8,
|
||||
y_stride: fmt.stride[0] as _,
|
||||
cr_stride: fmt.stride[2] as _,
|
||||
cb_stride: fmt.stride[1] as _,
|
||||
};
|
||||
let mut hdr: EbBufferHeaderType = unsafe { std::mem::zeroed() };
|
||||
hdr.size = std::mem::size_of::<EbBufferHeaderType>() as _;
|
||||
hdr.p_buffer = &mut io as *mut EbSvtIOFormat as *mut u8;
|
||||
hdr.n_filled_len = len as _;
|
||||
hdr.pts = ms;
|
||||
hdr.pic_type = EbAv1PictureType::EB_AV1_INVALID_PICTURE; // encoder decides
|
||||
|
||||
// send_picture copies the private data list, stack lifetime is fine.
|
||||
// The pending changes are only cleared after a successful send, so a
|
||||
// failed send retries them with the next picture.
|
||||
let pending_bitrate = self.pending_bitrate;
|
||||
let pending_fps = self.pending_fps;
|
||||
let mut rate_info = SvtAv1RateInfo {
|
||||
seq_qp: 0,
|
||||
target_bit_rate: pending_bitrate.unwrap_or(0).min(MAX_TARGET_BITRATE_KBPS) * 1000,
|
||||
};
|
||||
let mut fps_info = SvtAv1FrameRateInfo {
|
||||
frame_rate_numerator: pending_fps.unwrap_or(0),
|
||||
frame_rate_denominator: 1,
|
||||
};
|
||||
let mut rate_node = EbPrivDataNode {
|
||||
node_type: PrivDataType::RATE_CHANGE_EVENT,
|
||||
data: &mut rate_info as *mut SvtAv1RateInfo as *mut c_void,
|
||||
size: std::mem::size_of::<SvtAv1RateInfo>() as _,
|
||||
next: ptr::null_mut(),
|
||||
};
|
||||
let mut fps_node = EbPrivDataNode {
|
||||
node_type: PrivDataType::FRAME_RATE_CHANGE_EVENT,
|
||||
data: &mut fps_info as *mut SvtAv1FrameRateInfo as *mut c_void,
|
||||
size: std::mem::size_of::<SvtAv1FrameRateInfo>() as _,
|
||||
next: ptr::null_mut(),
|
||||
};
|
||||
let mut list: *mut EbPrivDataNode = ptr::null_mut();
|
||||
if pending_bitrate.is_some() {
|
||||
list = &mut rate_node;
|
||||
}
|
||||
if pending_fps.is_some() {
|
||||
fps_node.next = list;
|
||||
list = &mut fps_node;
|
||||
}
|
||||
hdr.p_app_private = list as *mut c_void;
|
||||
// The input YUV planes are copied inside send_picture.
|
||||
let res = unsafe { svt_av1_enc_send_picture(self.handle, &mut hdr) };
|
||||
if res != EbErrorType::EB_ErrorNone {
|
||||
bail!("svt_av1_enc_send_picture failed: {res:?}");
|
||||
}
|
||||
self.pending_bitrate = None;
|
||||
self.pending_fps = None;
|
||||
if let Some(fps) = pending_fps {
|
||||
self.fps = fps;
|
||||
}
|
||||
|
||||
// In low delay mode get_packet blocks until the packet for the picture just
|
||||
// sent is ready, and each picture produces exactly one packet. Do not call
|
||||
// it again without sending another picture, it would block forever.
|
||||
let mut frames = Vec::new();
|
||||
let mut pkt: *mut EbBufferHeaderType = ptr::null_mut();
|
||||
let res = unsafe { svt_av1_enc_get_packet(self.handle, &mut pkt, 0) };
|
||||
match res {
|
||||
EbErrorType::EB_ErrorNone if !pkt.is_null() => unsafe {
|
||||
let h = &*pkt;
|
||||
frames.push(EncodedVideoFrame {
|
||||
data: Bytes::from(
|
||||
slice::from_raw_parts(h.p_buffer, h.n_filled_len as usize).to_vec(),
|
||||
),
|
||||
key: h.pic_type == EbAv1PictureType::EB_AV1_KEY_PICTURE,
|
||||
pts: h.pts,
|
||||
..Default::default()
|
||||
});
|
||||
svt_av1_enc_release_out_buffer(&mut pkt);
|
||||
},
|
||||
EbErrorType::EB_NoErrorEmptyQueue => {}
|
||||
_ => {
|
||||
if !pkt.is_null() {
|
||||
unsafe {
|
||||
svt_av1_enc_release_out_buffer(&mut pkt);
|
||||
}
|
||||
}
|
||||
bail!("svt_av1_enc_get_packet failed: {res:?}");
|
||||
}
|
||||
}
|
||||
Ok(frames)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn create_video_frame(frames: Vec<EncodedVideoFrame>) -> VideoFrame {
|
||||
let mut vf = VideoFrame::new();
|
||||
let av1s = EncodedVideoFrames {
|
||||
frames: frames.into(),
|
||||
..Default::default()
|
||||
};
|
||||
vf.set_av1s(av1s);
|
||||
vf
|
||||
}
|
||||
|
||||
fn bitrate(width: u32, height: u32, ratio: f32) -> u32 {
|
||||
let bitrate = base_bitrate(width, height) as f32;
|
||||
(bitrate * ratio) as u32
|
||||
}
|
||||
|
||||
// Same mapping as AomEncoder::calc_q_values.
|
||||
#[inline]
|
||||
pub fn quality_qp_range(ratio: f32) -> (u32, u32) {
|
||||
let b = (ratio * 100.0) as u32;
|
||||
let b = std::cmp::min(b, 200);
|
||||
let q_min1 = 24;
|
||||
let q_min2 = 5;
|
||||
let q_max1 = 45;
|
||||
let q_max2 = 25;
|
||||
|
||||
let t = b as f32 / 200.0;
|
||||
|
||||
let mut q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32;
|
||||
let mut q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32;
|
||||
|
||||
q_min = q_min.clamp(q_min2, q_min1);
|
||||
q_max = q_max.clamp(q_max2, q_max1);
|
||||
|
||||
(q_min, q_max)
|
||||
}
|
||||
|
||||
fn get_yuvfmt(width: u32, height: u32) -> EncodeYuvFormat {
|
||||
let w = width as usize;
|
||||
let h = height as usize;
|
||||
let align = |x: usize| (x + STRIDE_ALIGN - 1) & !(STRIDE_ALIGN - 1);
|
||||
let stride_y = align(w);
|
||||
let stride_uv = align((w + 1) / 2);
|
||||
let u = stride_y * h;
|
||||
let v = u + stride_uv * ((h + 1) / 2);
|
||||
EncodeYuvFormat {
|
||||
pixfmt: Pixfmt::I420,
|
||||
w,
|
||||
h,
|
||||
stride: vec![stride_y, stride_uv, stride_uv],
|
||||
u,
|
||||
v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SvtAv1Encoder {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if self.handle.is_null() {
|
||||
return;
|
||||
}
|
||||
// svt_av1_enc_deinit drains the pipeline itself, but logs an error
|
||||
// when EOS was not sent first.
|
||||
let mut hdr: EbBufferHeaderType = std::mem::zeroed();
|
||||
hdr.size = std::mem::size_of::<EbBufferHeaderType>() as _;
|
||||
hdr.pic_type = EbAv1PictureType::EB_AV1_INVALID_PICTURE;
|
||||
hdr.flags = EB_BUFFERFLAG_EOS;
|
||||
let _ = svt_av1_enc_send_picture(self.handle, &mut hdr);
|
||||
let res = svt_av1_enc_deinit(self.handle);
|
||||
if res != EbErrorType::EB_ErrorNone {
|
||||
log::error!("svt_av1_enc_deinit failed: {res:?}");
|
||||
}
|
||||
let res = svt_av1_enc_deinit_handle(self.handle);
|
||||
if res != EbErrorType::EB_ErrorNone {
|
||||
log::error!("svt_av1_enc_deinit_handle failed: {res:?}");
|
||||
}
|
||||
self.handle = ptr::null_mut();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::common::GoogleImage;
|
||||
|
||||
fn fake_i420(fmt: &EncodeYuvFormat, index: usize) -> Vec<u8> {
|
||||
let chroma_height = (fmt.h + 1) / 2;
|
||||
let len = fmt.v + fmt.stride[2] * chroma_height;
|
||||
let mut yuv = vec![128u8; len];
|
||||
// moving gradient so every frame differs
|
||||
for y in 0..fmt.h {
|
||||
let row = &mut yuv[y * fmt.stride[0]..y * fmt.stride[0] + fmt.w];
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
*px = ((x + y + index * 17) % 256) as u8;
|
||||
}
|
||||
}
|
||||
yuv
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_and_decode_with_aom() {
|
||||
let (width, height) = (640u32, 480u32);
|
||||
let mut enc = SvtAv1Encoder::new(
|
||||
crate::codec::EncoderCfg::SVTAV1(SvtAv1EncoderConfig {
|
||||
width,
|
||||
height,
|
||||
quality: 1.0,
|
||||
keyframe_interval: None,
|
||||
qp_range: None,
|
||||
preset: None,
|
||||
}),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let fmt = enc.yuvfmt();
|
||||
let mut dec = crate::aom::AomDecoder::new().unwrap();
|
||||
for i in 0..20usize {
|
||||
if i == 5 {
|
||||
// exercises the FRAME_RATE_CHANGE_EVENT path on the next frame
|
||||
enc.set_fps(60);
|
||||
}
|
||||
if i == 10 {
|
||||
// exercises the RATE_CHANGE_EVENT path on the next frame
|
||||
enc.set_quality(0.5).unwrap();
|
||||
}
|
||||
let yuv = fake_i420(&fmt, i);
|
||||
let frames = enc.encode(i as i64 * 33, &yuv).unwrap();
|
||||
// one packet out per picture in, this is the low delay contract
|
||||
assert_eq!(frames.len(), 1, "no packet for frame {i}");
|
||||
assert_eq!(frames[0].key, i == 0, "unexpected key flag for frame {i}");
|
||||
assert!(!frames[0].data.is_empty());
|
||||
let mut decoded = 0;
|
||||
for f in &frames {
|
||||
for img in dec.decode(&f.data).unwrap() {
|
||||
assert_eq!(img.width(), width as usize);
|
||||
assert_eq!(img.height(), height as usize);
|
||||
decoded += 1;
|
||||
}
|
||||
}
|
||||
for img in dec.flush().unwrap() {
|
||||
assert_eq!(img.width(), width as usize);
|
||||
decoded += 1;
|
||||
}
|
||||
assert!(decoded >= 1, "aom failed to decode svt-av1 frame {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unsupported_resolution() {
|
||||
assert!(SvtAv1Encoder::new(
|
||||
crate::codec::EncoderCfg::SVTAV1(SvtAv1EncoderConfig {
|
||||
width: 62,
|
||||
height: 62,
|
||||
quality: 1.0,
|
||||
keyframe_interval: None,
|
||||
qp_range: None,
|
||||
preset: None,
|
||||
}),
|
||||
false,
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
7
res/vcpkg-triplets/arm-neon-android.cmake
Normal file
7
res/vcpkg-triplets/arm-neon-android.cmake
Normal file
@@ -0,0 +1,7 @@
|
||||
set(VCPKG_TARGET_ARCHITECTURE arm)
|
||||
set(VCPKG_CRT_LINKAGE dynamic)
|
||||
set(VCPKG_LIBRARY_LINKAGE static)
|
||||
set(VCPKG_CMAKE_SYSTEM_NAME Android)
|
||||
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
|
||||
set(VCPKG_MAKE_BUILD_TRIPLET "--host=armv7a-linux-androideabi")
|
||||
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=armeabi-v7a -DANDROID_ARM_NEON=ON)
|
||||
7
res/vcpkg-triplets/arm64-android.cmake
Normal file
7
res/vcpkg-triplets/arm64-android.cmake
Normal file
@@ -0,0 +1,7 @@
|
||||
set(VCPKG_TARGET_ARCHITECTURE arm64)
|
||||
set(VCPKG_CRT_LINKAGE dynamic)
|
||||
set(VCPKG_LIBRARY_LINKAGE static)
|
||||
set(VCPKG_CMAKE_SYSTEM_NAME Android)
|
||||
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
|
||||
set(VCPKG_MAKE_BUILD_TRIPLET "--host=aarch64-linux-android")
|
||||
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=arm64-v8a)
|
||||
7
res/vcpkg-triplets/x64-android.cmake
Normal file
7
res/vcpkg-triplets/x64-android.cmake
Normal file
@@ -0,0 +1,7 @@
|
||||
set(VCPKG_TARGET_ARCHITECTURE x64)
|
||||
set(VCPKG_CRT_LINKAGE dynamic)
|
||||
set(VCPKG_LIBRARY_LINKAGE static)
|
||||
set(VCPKG_CMAKE_SYSTEM_NAME Android)
|
||||
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
|
||||
set(VCPKG_MAKE_BUILD_TRIPLET "--host=x86_64-linux-android")
|
||||
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=x86_64)
|
||||
7
res/vcpkg-triplets/x86-android.cmake
Normal file
7
res/vcpkg-triplets/x86-android.cmake
Normal file
@@ -0,0 +1,7 @@
|
||||
set(VCPKG_TARGET_ARCHITECTURE x86)
|
||||
set(VCPKG_CRT_LINKAGE dynamic)
|
||||
set(VCPKG_LIBRARY_LINKAGE static)
|
||||
set(VCPKG_CMAKE_SYSTEM_NAME Android)
|
||||
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
|
||||
set(VCPKG_MAKE_BUILD_TRIPLET "--host=i686-linux-android")
|
||||
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=x86)
|
||||
10
res/vcpkg/svt-av1/no-force-llvm.diff
Normal file
10
res/vcpkg/svt-av1/no-force-llvm.diff
Normal file
@@ -0,0 +1,10 @@
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -194,5 +194,5 @@
|
||||
#Clang support, required to build static with LTO
|
||||
-if(CMAKE_C_COMPILER_ID MATCHES "Clang" AND UNIX AND NOT APPLE)
|
||||
+if(FALSE)
|
||||
find_program(LLVM_LD_EXE llvm-ld)
|
||||
find_program(LLVM_AR_EXE llvm-ar)
|
||||
find_program(LLVM_RANLIB_EXE llvm-ranlib)
|
||||
14
res/vcpkg/svt-av1/no-inline-yy_unpacklo_epi128.diff
Normal file
14
res/vcpkg/svt-av1/no-inline-yy_unpacklo_epi128.diff
Normal file
@@ -0,0 +1,14 @@
|
||||
diff --git a/Source/Lib/ASM_AVX2/synonyms_avx2.h b/Source/Lib/ASM_AVX2/synonyms_avx2.h
|
||||
--- a/Source/Lib/ASM_AVX2/synonyms_avx2.h
|
||||
+++ b/Source/Lib/ASM_AVX2/synonyms_avx2.h
|
||||
@@ -86,3 +86,9 @@
|
||||
-static INLINE __m256i yy_unpacklo_epi128(const __m256i in0, const __m256i in1) {
|
||||
+#if defined(_MSC_VER) && _MSC_VER >= 1950
|
||||
+#define MAYBE_INLINE NOINLINE
|
||||
+#else
|
||||
+#define MAYBE_INLINE INLINE
|
||||
+#endif
|
||||
+
|
||||
+static MAYBE_INLINE __m256i yy_unpacklo_epi128(const __m256i in0, const __m256i in1) {
|
||||
return _mm256_inserti128_si256(in0, _mm256_castsi256_si128(in1), 1);
|
||||
}
|
||||
45
res/vcpkg/svt-av1/portfile.cmake
Normal file
45
res/vcpkg/svt-av1/portfile.cmake
Normal file
@@ -0,0 +1,45 @@
|
||||
vcpkg_from_gitlab(
|
||||
GITLAB_URL https://gitlab.com
|
||||
OUT_SOURCE_PATH SOURCE_PATH
|
||||
REPO AOMediaCodec/SVT-AV1
|
||||
REF v${VERSION}
|
||||
SHA512 37df96559af179d28acae10cdff98c94ee2c4ee086b2446ab362b57be9adf566d6f2adc28faa30648798d9bc7d18eaeb2d0665e7292982652496b605342e1c4b
|
||||
PATCHES
|
||||
# upstream forces llvm-ld/llvm-ar/llvm-ranlib for clang static builds on
|
||||
# non-Apple unix, which breaks toolchains without the llvm binutils
|
||||
no-force-llvm.diff
|
||||
# MSVC >= 1950 miscompiles the inlined yy_unpacklo_epi128
|
||||
no-inline-yy_unpacklo_epi128.diff
|
||||
)
|
||||
|
||||
if(VCPKG_TARGET_ARCHITECTURE MATCHES "^(x86|x64)$")
|
||||
# NASM is required to build the x86/x64 assembly
|
||||
vcpkg_find_acquire_program(NASM)
|
||||
set(SIMD_OPTIONS -DCOMPILE_C_ONLY=OFF "-DCMAKE_ASM_NASM_COMPILER=${NASM}")
|
||||
elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "^(arm64|arm64ec)$" AND NOT VCPKG_TARGET_IS_WINDOWS)
|
||||
set(SIMD_OPTIONS -DCOMPILE_C_ONLY=OFF)
|
||||
else()
|
||||
set(SIMD_OPTIONS -DCOMPILE_C_ONLY=ON)
|
||||
endif()
|
||||
|
||||
vcpkg_cmake_configure(
|
||||
SOURCE_PATH "${SOURCE_PATH}"
|
||||
OPTIONS
|
||||
${SIMD_OPTIONS}
|
||||
-DBUILD_APPS=OFF
|
||||
-DBUILD_TESTING=OFF
|
||||
-DREPRODUCIBLE_BUILDS=ON
|
||||
# SVT-AV1 defaults LTO to ON for gcc>=9/clang>=12, which puts LTO
|
||||
# bitcode into the static archive and breaks linking with a different
|
||||
# compiler version
|
||||
-DSVT_AV1_LTO=OFF
|
||||
)
|
||||
|
||||
vcpkg_cmake_install()
|
||||
vcpkg_cmake_config_fixup(PACKAGE_NAME SVT-AV1 CONFIG_PATH lib/cmake/SVT-AV1)
|
||||
vcpkg_copy_pdbs()
|
||||
vcpkg_fixup_pkgconfig()
|
||||
|
||||
file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include")
|
||||
|
||||
vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.md" "${SOURCE_PATH}/PATENTS.md")
|
||||
19
res/vcpkg/svt-av1/vcpkg.json
Normal file
19
res/vcpkg/svt-av1/vcpkg.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "svt-av1",
|
||||
"version-semver": "4.2.0",
|
||||
"port-version": 0,
|
||||
"description": "Scalable Video Technology AV1 software video encoder library",
|
||||
"homepage": "https://gitlab.com/AOMediaCodec/SVT-AV1",
|
||||
"license": "BSD-3-Clause-Clear",
|
||||
"supports": "!x86 & !arm32 & !uwp",
|
||||
"dependencies": [
|
||||
{
|
||||
"name": "vcpkg-cmake",
|
||||
"host": true
|
||||
},
|
||||
{
|
||||
"name": "vcpkg-cmake-config",
|
||||
"host": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1024,7 +1024,7 @@ pub fn get_full_name() -> String {
|
||||
}
|
||||
|
||||
pub fn is_setup(name: &str) -> bool {
|
||||
name.to_lowercase().ends_with("install.exe")
|
||||
!config::is_disable_installation() && name.to_lowercase().ends_with("install.exe")
|
||||
}
|
||||
|
||||
pub fn get_custom_rendezvous_server(custom: String) -> String {
|
||||
@@ -2623,6 +2623,20 @@ pub fn is_direct_ip_access(peer: &str) -> bool {
|
||||
hbb_common::is_ip_str(peer) || hbb_common::is_domain_port_str(peer)
|
||||
}
|
||||
|
||||
// Align the maximum length of the peer id to the maximum length of the peer id in the server.
|
||||
const MAX_UNTRUSTED_PEER_ID_LEN: usize = 253;
|
||||
const UNTRUSTED_PEER_ID_FORBIDDEN_CHARS: &[char] = &['"', '<', '>', '/', '\\', '|', '?', '*'];
|
||||
|
||||
// Shared validation for peer/connect ids that cross untrusted boundaries before
|
||||
// they are stored or written into command/script contexts.
|
||||
pub fn is_valid_untrusted_peer_id(id: &str) -> bool {
|
||||
!id.is_empty()
|
||||
&& id.len() <= MAX_UNTRUSTED_PEER_ID_LEN
|
||||
&& !id.chars().any(|ch| {
|
||||
ch.is_control() || ch.is_whitespace() || UNTRUSTED_PEER_ID_FORBIDDEN_CHARS.contains(&ch)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -2653,6 +2667,29 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn untrusted_peer_id_validation() {
|
||||
let cases = [
|
||||
("123456789", true),
|
||||
("m\u{00FC}nchen-pc", true),
|
||||
("192.168.1.10:21118", true),
|
||||
("9123456234@public", true),
|
||||
(
|
||||
r#"1" & oWS.Run("cmd.exe /k whoami /priv",1,False) & ""#,
|
||||
false,
|
||||
),
|
||||
("", false),
|
||||
("peer id", false),
|
||||
("peer\nid", false),
|
||||
("peer/id", false),
|
||||
("peer?id", false),
|
||||
];
|
||||
|
||||
for (id, expected) in cases {
|
||||
assert_eq!(is_valid_untrusted_peer_id(id), expected, "{id:?}");
|
||||
}
|
||||
}
|
||||
|
||||
// ThrottledInterval tick at the same time as tokio interval, if no sleeps
|
||||
#[allow(non_snake_case)]
|
||||
#[tokio::test]
|
||||
|
||||
@@ -127,6 +127,13 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
if args.contains(&"--noinstall".to_string()) {
|
||||
args.clear();
|
||||
}
|
||||
// The portable wrapper injects `--install` when its name ends with `install.exe`,
|
||||
// including `no-install.exe`. Drop the argument instead of exiting so disabled
|
||||
// clients can continue running as portable applications.
|
||||
if config::is_disable_installation() {
|
||||
args.retain(|arg| arg != "--install");
|
||||
flutter_args.retain(|arg| arg != "--install");
|
||||
}
|
||||
if args.len() > 0 {
|
||||
if args[0] == "--version" {
|
||||
println!("{}", crate::VERSION);
|
||||
@@ -660,7 +667,8 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
None
|
||||
}
|
||||
};
|
||||
let new_id = get_value("--id");
|
||||
// An empty --id (e.g. an unset var) would deploy a blank id; the Android flow guards this too (#15146).
|
||||
let new_id = get_value("--id").filter(|s| !s.is_empty());
|
||||
match crate::ui_interface::deploy_device(token, new_id) {
|
||||
crate::ui_interface::DeployResult::Ok => {
|
||||
println!("Device deployed.");
|
||||
|
||||
@@ -136,6 +136,12 @@ pub extern "C" fn rustdesk_core_main_args(args_len: *mut c_int) -> *mut *mut c_c
|
||||
return std::ptr::null_mut() as _;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn rustdesk_is_disable_installation() -> c_int {
|
||||
hbb_common::config::is_disable_installation() as c_int
|
||||
}
|
||||
|
||||
// https://gist.github.com/iskakaushik/1c5b8aa75c77479c33c4320913eebef6
|
||||
#[cfg(windows)]
|
||||
fn rust_args_to_c_args(args: Vec<String>, outlen: *mut c_int) -> *mut *mut c_char {
|
||||
|
||||
@@ -9,8 +9,8 @@ mod http_client;
|
||||
pub mod record_upload;
|
||||
pub mod sync;
|
||||
pub use http_client::{
|
||||
create_http_client_async, create_http_client_async_with_url, create_http_client_with_url,
|
||||
get_url_for_tls,
|
||||
create_http_client_async, create_http_client_async_with_url_strict,
|
||||
create_http_client_with_url, create_http_client_with_url_strict, get_url_for_tls,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::create_http_client_async_with_url;
|
||||
use super::create_http_client_async_with_url_strict;
|
||||
use hbb_common::{
|
||||
bail,
|
||||
lazy_static::lazy_static,
|
||||
@@ -167,7 +167,7 @@ async fn do_download(
|
||||
auto_del_dur: Option<Duration>,
|
||||
mut rx_cancel: UnboundedReceiver<()>,
|
||||
) -> ResultType<bool> {
|
||||
let client = create_http_client_async_with_url(&url).await;
|
||||
let client = create_http_client_async_with_url_strict(&url).await?;
|
||||
|
||||
let mut is_all_downloaded = false;
|
||||
tokio::select! {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use hbb_common::{
|
||||
async_recursion::async_recursion,
|
||||
bail,
|
||||
config::{Config, Socks5Server},
|
||||
log::{self, info},
|
||||
proxy::{Proxy, ProxyScheme},
|
||||
@@ -7,6 +8,7 @@ use hbb_common::{
|
||||
get_cached_tls_accept_invalid_cert, get_cached_tls_type, is_plain, upsert_tls_cache,
|
||||
TlsType,
|
||||
},
|
||||
ResultType,
|
||||
};
|
||||
use reqwest::{blocking::Client as SyncClient, Client as AsyncClient};
|
||||
|
||||
@@ -137,6 +139,32 @@ pub fn create_http_client_with_url(url: &str) -> SyncClient {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_http_client_with_url_strict(url: &str) -> ResultType<SyncClient> {
|
||||
let parsed_url = url::Url::parse(url)?;
|
||||
if parsed_url.scheme() != "https" {
|
||||
bail!("Strict HTTP client requires HTTPS: {}", url);
|
||||
}
|
||||
let proxy_conf = Config::get_socks();
|
||||
let tls_url = get_url_for_tls(url, &proxy_conf);
|
||||
let cached_tls_type = get_cached_tls_type(tls_url);
|
||||
let cached_danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
||||
let can_reuse_cached_probe =
|
||||
cached_tls_type.is_some() && cached_danger_accept_invalid_cert == Some(false);
|
||||
let tls_type = if can_reuse_cached_probe {
|
||||
cached_tls_type.unwrap_or(TlsType::Rustls)
|
||||
} else {
|
||||
TlsType::Rustls
|
||||
};
|
||||
Ok(create_http_client_with_url_(
|
||||
url,
|
||||
tls_url,
|
||||
tls_type,
|
||||
can_reuse_cached_probe,
|
||||
Some(false),
|
||||
Some(false),
|
||||
))
|
||||
}
|
||||
|
||||
fn create_http_client_with_url_(
|
||||
url: &str,
|
||||
tls_url: &str,
|
||||
@@ -247,6 +275,33 @@ pub async fn create_http_client_async_with_url(url: &str) -> AsyncClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_http_client_async_with_url_strict(url: &str) -> ResultType<AsyncClient> {
|
||||
let parsed_url = url::Url::parse(url)?;
|
||||
if parsed_url.scheme() != "https" {
|
||||
bail!("Strict HTTP client requires HTTPS: {}", url);
|
||||
}
|
||||
let proxy_conf = Config::get_socks();
|
||||
let tls_url = get_url_for_tls(url, &proxy_conf);
|
||||
let cached_tls_type = get_cached_tls_type(tls_url);
|
||||
let cached_danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
||||
let can_reuse_cached_probe =
|
||||
cached_tls_type.is_some() && cached_danger_accept_invalid_cert == Some(false);
|
||||
let tls_type = if can_reuse_cached_probe {
|
||||
cached_tls_type.unwrap_or(TlsType::Rustls)
|
||||
} else {
|
||||
TlsType::Rustls
|
||||
};
|
||||
Ok(create_http_client_async_with_url_(
|
||||
url,
|
||||
tls_url,
|
||||
tls_type,
|
||||
can_reuse_cached_probe,
|
||||
Some(false),
|
||||
Some(false),
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
async fn create_http_client_async_with_url_(
|
||||
url: &str,
|
||||
|
||||
35
src/ipc.rs
35
src/ipc.rs
@@ -881,8 +881,14 @@ async fn handle(data: Data, stream: &mut Connection) {
|
||||
Some(value) => {
|
||||
let mut updated = true;
|
||||
if name == "id" {
|
||||
Config::set_key_confirmed(false);
|
||||
Config::set_id(&value);
|
||||
// An empty id would wipe the local id and unconfirm the key (cf. #15626).
|
||||
if value.is_empty() {
|
||||
log::warn!("Ignoring empty id write over IPC");
|
||||
updated = false;
|
||||
} else {
|
||||
Config::set_key_confirmed(false);
|
||||
Config::set_id(&value);
|
||||
}
|
||||
} else if name == "temporary-password" {
|
||||
password::update_temporary_password();
|
||||
} else if name == "permanent-password" {
|
||||
@@ -1689,19 +1695,24 @@ pub fn clear_trusted_devices() {
|
||||
}
|
||||
|
||||
pub fn get_id() -> String {
|
||||
// An empty id may come from a process that took over the main IPC with a
|
||||
// config scope that has no id yet (e.g. a user GUI that became the server
|
||||
// while the installed service was restarting). Treat it as no answer,
|
||||
// otherwise the empty id is adopted below and wipes the local one.
|
||||
if let Ok(Some(v)) = get_config("id") {
|
||||
// update salt also, so that next time reinstallation not causing first-time auto-login failure
|
||||
if let Ok(Some(v2)) = get_config("salt") {
|
||||
Config::set_salt(&v2);
|
||||
if !v.is_empty() {
|
||||
// update salt also, so that next time reinstallation not causing first-time auto-login failure
|
||||
if let Ok(Some(v2)) = get_config("salt") {
|
||||
Config::set_salt(&v2);
|
||||
}
|
||||
if v != Config::get_id() {
|
||||
Config::set_key_confirmed(false);
|
||||
Config::set_id(&v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
if v != Config::get_id() {
|
||||
Config::set_key_confirmed(false);
|
||||
Config::set_id(&v);
|
||||
}
|
||||
v
|
||||
} else {
|
||||
Config::get_id()
|
||||
}
|
||||
Config::get_id()
|
||||
}
|
||||
|
||||
pub async fn get_rendezvous_server(ms_timeout: u64) -> (String, Vec<String>) {
|
||||
|
||||
@@ -241,6 +241,14 @@ fn wait_response(
|
||||
Some(rendezvous_message::Union::PeerDiscovery(p)) => {
|
||||
last_recv_time = Instant::now();
|
||||
if p.cmd == "pong" {
|
||||
if !crate::common::is_valid_untrusted_peer_id(&p.id) {
|
||||
log::warn!(
|
||||
"Ignoring LAN discovery response from {} with invalid peer id",
|
||||
addr
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let local_mac = if try_get_ip_by_peer {
|
||||
if let Some(self_addr) = get_ipaddr_by_peer(&addr) {
|
||||
get_mac(&self_addr)
|
||||
|
||||
@@ -332,7 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "中继连接"),
|
||||
("Secure Connection", "安全连接"),
|
||||
("Insecure Connection", "非安全连接"),
|
||||
("Continue", ""),
|
||||
("Continue", "继续"),
|
||||
("Scale original", "原始尺寸"),
|
||||
("Scale adaptive", "适应窗口"),
|
||||
("General", "常规"),
|
||||
|
||||
@@ -332,7 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Connessione relay"),
|
||||
("Secure Connection", "Connessione sicura"),
|
||||
("Insecure Connection", "Connessione non sicura"),
|
||||
("Continue", ""),
|
||||
("Continue", "Continua"),
|
||||
("Scale original", "Scala originale"),
|
||||
("Scale adaptive", "Scala adattiva"),
|
||||
("General", "Generale"),
|
||||
@@ -764,6 +764,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show on the minimized toolbar", "Visualizza nella barra strumenti ridotta a icona"),
|
||||
("All monitors", "Tutti gli schermi"),
|
||||
("#{} monitor", "Schermo {}"),
|
||||
("conn-e2ee-unavailable-tip", "Impossibile verificare la crittografia end-to-end.\nIl dispositivo remoto potrebbe essere ancora in configurazione. Riprova più tardi.\nSe il problema persiste, il server potrebbe non essere attendibile.\nContinuare comunque?"),
|
||||
("conn-e2ee-unavailable-tip", "Impossibile verificare la crittografia end-to-end.\nIl dispositivo remoto potrebbe essere ancora in configurazione. Riprova più tardi.\nSe il problema persiste, il server potrebbe non essere attendibile.\nVuoi continuare?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("id_change_tip", "a-z, A-Z, 0-9, -(대시) 및 _(밑줄) 문자만 허용됩니다. 첫 글자는 a-z, A-Z여야 합니다. 길이는 6에서 16 사이여야 합니다."),
|
||||
("Website", "웹사이트"),
|
||||
("About", "정보"),
|
||||
("Slogan_tip", "이 혼란스러운 세상에서 마음을 담아 만들었습니다!"),
|
||||
("Slogan_tip", "이 혼란스러운 세상에서 마음을 담아 만들었습니다! - 한국어 번역: 비너스걸"),
|
||||
("Privacy Statement", "개인정보 보호정책"),
|
||||
("Mute", "음소거"),
|
||||
("Build Date", "빌드 날짜"),
|
||||
@@ -332,7 +332,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "릴레이 연결"),
|
||||
("Secure Connection", "보안 연결"),
|
||||
("Insecure Connection", "보안되지 않은 연결"),
|
||||
("Continue", ""),
|
||||
("Scale original", "원본 크기 조정"),
|
||||
("Scale adaptive", "크기 조정 가능"),
|
||||
("General", "일반"),
|
||||
@@ -764,6 +763,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show on the minimized toolbar", "최소화된 도구 모음에 표시"),
|
||||
("All monitors", "모든 모니터"),
|
||||
("#{} monitor", "#{} 모니터"),
|
||||
("conn-e2ee-unavailable-tip", "종단 간 암호화를 확인할 수 없습니다.\n원격 장치가 아직 설정 중일 수 있습니다. 나중에 다시 시도하세요.\n이 문제가 계속되면 서버를 신뢰할 수 없을 수 있습니다.\n그래도 계속하시겠습니까?"),
|
||||
("conn-e2ee-unavailable-tip", "종단 간 암호화를 확인할 수 없습니다.\n원격 장치가 여전히 설정 중일 수 있습니다. 나중에 다시 시도해 보세요.\n이런 일이 계속 발생하면 서버가 신뢰할 수 없을 수도 있습니다.\n어쨌든 계속하시겠습니까?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Połączenie przez bramkę"),
|
||||
("Secure Connection", "Połączenie szyfrowane"),
|
||||
("Insecure Connection", "Połączenie nieszyfrowane"),
|
||||
("Continue", ""),
|
||||
("Continue", "Kontynuuj"),
|
||||
("Scale original", "Skalowanie oryginalne"),
|
||||
("Scale adaptive", "Dopasuj do wyświetlacza"),
|
||||
("General", "Ogólne"),
|
||||
|
||||
@@ -112,7 +112,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Waiting", "Aguardando"),
|
||||
("Finished", "Concluído"),
|
||||
("Speed", "Velocidade"),
|
||||
("Custom Image Quality", "Qualidade Visual Personalizada"),
|
||||
("Custom Image Quality", "Qualidade de imagem personalizada"),
|
||||
("Privacy mode", "Modo privado"),
|
||||
("Block user input", "Bloquear entrada do usuário"),
|
||||
("Unblock user input", "Desbloquear entrada do usuário"),
|
||||
@@ -122,15 +122,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Stretch", "Aumentar"),
|
||||
("Scrollbar", "Barra de rolagem"),
|
||||
("ScrollAuto", "Rolagem automática"),
|
||||
("Good image quality", "Boa qualidade de imagem"),
|
||||
("Good image quality", "Melhor qualidade"),
|
||||
("Balanced", "Balanceada"),
|
||||
("Optimize reaction time", "Otimizar tempo de resposta"),
|
||||
("Custom", "Personalizado"),
|
||||
("Custom", "Personalizada"),
|
||||
("Show remote cursor", "Mostrar cursor remoto"),
|
||||
("Show quality monitor", "Exibir monitor de qualidade"),
|
||||
("Disable clipboard", "Desabilitar área de transferência"),
|
||||
("Lock after session end", "Bloquear após o fim da sessão"),
|
||||
("Insert Ctrl + Alt + Del", "Enviar Ctrl + Alt + Del"),
|
||||
("Insert Ctrl + Alt + Del", "Enviar Ctrl+Alt+Del"),
|
||||
("Insert Lock", "Bloquear computador"),
|
||||
("Refresh", "Atualizar"),
|
||||
("ID does not exist", "ID não existe"),
|
||||
@@ -332,7 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Conexão via Relay"),
|
||||
("Secure Connection", "Conexão Segura"),
|
||||
("Insecure Connection", "Conexão Insegura"),
|
||||
("Continue", ""),
|
||||
("Continue", "Continuar"),
|
||||
("Scale original", "Escala original"),
|
||||
("Scale adaptive", "Escala adaptada"),
|
||||
("General", "Geral"),
|
||||
|
||||
@@ -646,6 +646,16 @@ fn try_start_server_(desktop: Option<&Desktop>) -> ResultType<Option<Child>> {
|
||||
if !desktop.dbus.is_empty() {
|
||||
envs.push(("DBUS_SESSION_BUS_ADDRESS", desktop.dbus.clone()));
|
||||
}
|
||||
if let Ok(forced_display_server) =
|
||||
std::env::var("RUSTDESK_FORCED_DISPLAY_SERVER")
|
||||
{
|
||||
if !forced_display_server.is_empty() {
|
||||
envs.push((
|
||||
"RUSTDESK_FORCED_DISPLAY_SERVER",
|
||||
forced_display_server,
|
||||
));
|
||||
}
|
||||
}
|
||||
envs.push((
|
||||
"TERM",
|
||||
get_cur_term(&desktop.uid).unwrap_or_else(|| suggest_best_term()),
|
||||
|
||||
@@ -118,16 +118,18 @@ extern "C" bool MacCheckAdminAuthorization() {
|
||||
|
||||
// https://gist.github.com/briankc/025415e25900750f402235dbf1b74e42
|
||||
extern "C" float BackingScaleFactor(uint32_t display) {
|
||||
NSArray<NSScreen *> *screens = [NSScreen screens];
|
||||
for (NSScreen *screen in screens) {
|
||||
NSDictionary *deviceDescription = [screen deviceDescription];
|
||||
NSNumber *screenNumber = [deviceDescription objectForKey:@"NSScreenNumber"];
|
||||
CGDirectDisplayID screenDisplayID = [screenNumber unsignedIntValue];
|
||||
if (screenDisplayID == display) {
|
||||
return [screen backingScaleFactor];
|
||||
@autoreleasepool {
|
||||
NSArray<NSScreen *> *screens = [NSScreen screens];
|
||||
for (NSScreen *screen in screens) {
|
||||
NSDictionary *deviceDescription = [screen deviceDescription];
|
||||
NSNumber *screenNumber = [deviceDescription objectForKey:@"NSScreenNumber"];
|
||||
CGDirectDisplayID screenDisplayID = [screenNumber unsignedIntValue];
|
||||
if (screenDisplayID == display) {
|
||||
return [screen backingScaleFactor];
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// https://github.com/jhford/screenresolution/blob/master/cg_utils.c
|
||||
|
||||
@@ -2278,6 +2278,10 @@ fn get_shortcut_icon_location(install_dir: &str, exe: &str) -> String {
|
||||
}
|
||||
|
||||
pub fn create_shortcut(id: &str) -> ResultType<()> {
|
||||
if !crate::common::is_valid_untrusted_peer_id(id) {
|
||||
bail!("Invalid peer id for shortcut");
|
||||
}
|
||||
|
||||
let exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned();
|
||||
// https://github.com/rustdesk/rustdesk/issues/13735
|
||||
// Replace ':' with '_' for filename since ':' is not allowed in Windows filenames
|
||||
|
||||
@@ -2807,7 +2807,6 @@ impl Connection {
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if let Some(lr) = _s.lr.clone().take() {
|
||||
self.handle_login_request_without_validation(&lr).await;
|
||||
SWITCH_SIDES_UUID
|
||||
.lock()
|
||||
.unwrap()
|
||||
@@ -2816,6 +2815,15 @@ impl Connection {
|
||||
if let Ok(uuid) = uuid::Uuid::from_slice(_s.uuid.to_vec().as_ref()) {
|
||||
if let Some((_instant, uuid_old)) = uuid_old {
|
||||
if uuid == uuid_old {
|
||||
if lr.union.is_some() {
|
||||
log::warn!(
|
||||
"Rejected switch sides response for non-remote-desktop session; closing connection"
|
||||
);
|
||||
self.send_login_error("Connection not allowed").await;
|
||||
return false;
|
||||
}
|
||||
self.reset_session_scope_for_login();
|
||||
self.handle_login_request_without_validation(&lr).await;
|
||||
self.from_switch = true;
|
||||
self.set_conn_audit_primary_auth(ConnAuditPrimaryAuth::SwitchSides);
|
||||
if !self.send_logon_response_and_keep_alive().await {
|
||||
@@ -5656,6 +5664,7 @@ impl Connection {
|
||||
Some(misc::Union::ChangeDisplayResolution(_)) => "misc.change_display_resolution",
|
||||
Some(misc::Union::MessageQuery(_)) => "misc.message_query",
|
||||
Some(misc::Union::FollowCurrentDisplay(_)) => "misc.follow_current_display",
|
||||
Some(misc::Union::SwitchSidesRequest(_)) => "misc.switch_sides_request",
|
||||
Some(_) => "misc.other",
|
||||
None => "misc.empty",
|
||||
}
|
||||
@@ -6773,6 +6782,10 @@ mod test {
|
||||
misc_msg(|m| m.set_capture_displays(CaptureDisplays::new())),
|
||||
Some("misc.capture_displays"),
|
||||
),
|
||||
(
|
||||
misc_msg(|m| m.set_switch_sides_request(SwitchSidesRequest::new())),
|
||||
Some("misc.switch_sides_request"),
|
||||
),
|
||||
(msg(|m| m.set_clipboard(Clipboard::new())), None),
|
||||
(
|
||||
msg(|m| m.set_multi_clipboards(MultiClipboards::new())),
|
||||
@@ -6817,6 +6830,10 @@ mod test {
|
||||
misc_msg(|m| m.set_toggle_privacy_mode(TogglePrivacyMode::new())),
|
||||
Some("misc.toggle_privacy_mode"),
|
||||
),
|
||||
(
|
||||
misc_msg(|m| m.set_switch_sides_request(SwitchSidesRequest::new())),
|
||||
Some("misc.switch_sides_request"),
|
||||
),
|
||||
(misc_msg(|m| m.set_chat_message(ChatMessage::new())), None),
|
||||
(msg(|m| m.set_clipboard(Clipboard::new())), None),
|
||||
(
|
||||
@@ -6902,6 +6919,10 @@ mod test {
|
||||
msg(|m| m.set_terminal_action(TerminalAction::new())),
|
||||
Some("terminal_action"),
|
||||
),
|
||||
(
|
||||
misc_msg(|m| m.set_switch_sides_request(SwitchSidesRequest::new())),
|
||||
Some("misc.switch_sides_request"),
|
||||
),
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -6912,6 +6933,10 @@ mod test {
|
||||
None,
|
||||
),
|
||||
(msg(|m| m.set_terminal_action(TerminalAction::new())), None),
|
||||
(
|
||||
misc_msg(|m| m.set_switch_sides_request(SwitchSidesRequest::new())),
|
||||
None,
|
||||
),
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -6931,6 +6956,10 @@ mod test {
|
||||
msg(|m| m.set_screenshot_request(ScreenshotRequest::new())),
|
||||
Some("screenshot_request"),
|
||||
),
|
||||
(
|
||||
misc_msg(|m| m.set_switch_sides_request(SwitchSidesRequest::new())),
|
||||
Some("misc.switch_sides_request"),
|
||||
),
|
||||
(misc_msg(|m| m.set_refresh_video(true)), None),
|
||||
(misc_msg(|m| m.set_refresh_video_display(0)), None),
|
||||
(
|
||||
|
||||
@@ -1023,12 +1023,40 @@ fn get_encoder_config(
|
||||
},
|
||||
keyframe_interval,
|
||||
}),
|
||||
CodecFormat::AV1 => EncoderCfg::AOM(AomEncoderConfig {
|
||||
width: c.width as _,
|
||||
height: c.height as _,
|
||||
quality,
|
||||
keyframe_interval,
|
||||
}),
|
||||
CodecFormat::AV1 => {
|
||||
let aom = EncoderCfg::AOM(AomEncoderConfig {
|
||||
width: c.width as _,
|
||||
height: c.height as _,
|
||||
quality,
|
||||
keyframe_interval,
|
||||
});
|
||||
// Prefer svt-av1 for AV1 encoding, keep aom for i444 (svt-av1 is 4:2:0
|
||||
// only) and for resolutions svt-av1 cannot handle.
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
{
|
||||
let use_i444 = Encoder::use_i444(&aom);
|
||||
if scrap::svt_av1::SvtAv1Encoder::support(c.width as _, c.height as _) && !use_i444
|
||||
{
|
||||
return EncoderCfg::SVTAV1(scrap::svt_av1::SvtAv1EncoderConfig {
|
||||
width: c.width as _,
|
||||
height: c.height as _,
|
||||
quality,
|
||||
keyframe_interval,
|
||||
qp_range: None,
|
||||
preset: None,
|
||||
});
|
||||
}
|
||||
log::info!(
|
||||
"AV1 uses aom instead of svt-av1: {}",
|
||||
if use_i444 {
|
||||
"i444"
|
||||
} else {
|
||||
"unsupported resolution"
|
||||
}
|
||||
);
|
||||
}
|
||||
aom
|
||||
}
|
||||
_ => EncoderCfg::VPX(VpxEncoderConfig {
|
||||
width: c.width as _,
|
||||
height: c.height as _,
|
||||
@@ -1326,6 +1354,9 @@ fn check_qos(
|
||||
) -> ResultType<()> {
|
||||
let mut video_qos = VIDEO_QOS.lock().unwrap();
|
||||
*spf = video_qos.spf();
|
||||
// encoders whose rate control budgets per frame need the real pacing rate,
|
||||
// implementations no-op when the value is unchanged
|
||||
encoder.set_fps(video_qos.fps());
|
||||
if *ratio != video_qos.ratio() {
|
||||
*ratio = video_qos.ratio();
|
||||
if encoder.support_changing_quality() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{common::do_check_software_update, hbbs_http::create_http_client_with_url};
|
||||
use crate::{common::do_check_software_update, hbbs_http::create_http_client_with_url_strict};
|
||||
use hbb_common::{bail, config, log, ResultType};
|
||||
use std::{
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
path::{Component, Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
@@ -153,7 +153,7 @@ fn check_update(manually: bool) -> ResultType<()> {
|
||||
format!("{}/rustdesk-{}-x86-sciter.exe", download_url, version)
|
||||
};
|
||||
log::debug!("New version available: {}", &version);
|
||||
let client = create_http_client_with_url(&download_url);
|
||||
let client = create_http_client_with_url_strict(&download_url)?;
|
||||
let Some(file_path) = get_download_file_from_url(&download_url) else {
|
||||
bail!("Failed to get the file path from the URL: {}", download_url);
|
||||
};
|
||||
@@ -291,7 +291,96 @@ fn update_new_version(update_msi: bool, version: &str, file_path: &PathBuf) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_download_file_from_url(url: &str) -> Option<PathBuf> {
|
||||
let filename = url.split('/').last()?;
|
||||
pub fn get_update_download_file_from_url(url: &str) -> Option<PathBuf> {
|
||||
let parsed = url::Url::parse(url).ok()?;
|
||||
// Check the raw prefix before Url normalizes default ports.
|
||||
if !url.starts_with("https://github.com/")
|
||||
|| parsed.scheme() != "https"
|
||||
|| parsed.host_str() != Some("github.com")
|
||||
|| !parsed.username().is_empty()
|
||||
|| parsed.password().is_some()
|
||||
|| parsed.port().is_some()
|
||||
|| parsed.query().is_some()
|
||||
|| parsed.fragment().is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut segments = parsed.path_segments()?;
|
||||
let owner = segments.next()?;
|
||||
let repo = segments.next()?;
|
||||
let releases = segments.next()?;
|
||||
let download = segments.next()?;
|
||||
let tag = segments.next()?;
|
||||
let filename = segments.next()?;
|
||||
|
||||
if owner != "rustdesk"
|
||||
|| repo != "rustdesk"
|
||||
|| releases != "releases"
|
||||
|| download != "download"
|
||||
|| tag.is_empty()
|
||||
|| segments.next().is_some()
|
||||
|| !is_plain_update_filename(filename)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(std::env::temp_dir().join(filename))
|
||||
}
|
||||
|
||||
fn is_plain_update_filename(filename: &str) -> bool {
|
||||
if filename.is_empty()
|
||||
|| filename.contains('/')
|
||||
|| filename.contains('\\')
|
||||
|| filename.contains(':')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut components = Path::new(filename).components();
|
||||
matches!(
|
||||
components.next(),
|
||||
Some(Component::Normal(name)) if name.to_str() == Some(filename)
|
||||
) && components.next().is_none()
|
||||
}
|
||||
|
||||
pub fn get_download_file_from_url(url: &str) -> Option<PathBuf> {
|
||||
get_update_download_file_from_url(url)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::get_download_file_from_url;
|
||||
|
||||
#[test]
|
||||
fn update_download_file_accepts_expected_github_asset_urls() {
|
||||
let file = get_download_file_from_url(
|
||||
"https://github.com/rustdesk/rustdesk/releases/download/1.4.0/rustdesk-1.4.0-x86_64.dmg",
|
||||
)
|
||||
.expect("valid GitHub release asset URL");
|
||||
|
||||
assert_eq!(
|
||||
file.file_name().and_then(|name| name.to_str()),
|
||||
Some("rustdesk-1.4.0-x86_64.dmg")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_download_file_rejects_untrusted_or_malformed_urls() {
|
||||
for url in [
|
||||
"http://github.com/rustdesk/rustdesk/releases/download/1/rustdesk.exe",
|
||||
"https://example.com/rustdesk.exe",
|
||||
"https://github.com/other/project/releases/download/1/rustdesk.exe",
|
||||
"https://github.com/rustdesk/rustdesk/releases/download/1/",
|
||||
"https://github.com/rustdesk/rustdesk/releases/download/1/nested/rustdesk.exe",
|
||||
"https://github.com/rustdesk/rustdesk/releases/download/1/C:rustdesk.exe",
|
||||
"https://user@github.com/rustdesk/rustdesk/releases/download/1/rustdesk.exe",
|
||||
"https://github.com:443/rustdesk/rustdesk/releases/download/1/rustdesk.exe",
|
||||
"https://github.com/rustdesk/rustdesk/releases/download/1/rustdesk.exe?download=1",
|
||||
"https://github.com/rustdesk/rustdesk/releases/download/1/rustdesk.exe#download",
|
||||
"not a url",
|
||||
] {
|
||||
assert!(get_download_file_from_url(url).is_none(), "{url}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
#include <check.h>
|
||||
#include <stdlib.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "../libs/clipboard/src/windows/wf_cliprdr.c"
|
||||
|
||||
static SIZE_T descriptor_size(UINT count)
|
||||
{
|
||||
return offsetof(FILEGROUPDESCRIPTORW, fgd) + (SIZE_T)count * sizeof(FILEDESCRIPTORW);
|
||||
}
|
||||
|
||||
START_TEST(test_descriptor_size_rejects_buffer_smaller_than_header)
|
||||
{
|
||||
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid(0, 1), FALSE);
|
||||
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid(
|
||||
offsetof(FILEGROUPDESCRIPTORW, fgd) - 1, 1),
|
||||
FALSE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_descriptor_size_rejects_zero_items)
|
||||
{
|
||||
ck_assert_int_eq(
|
||||
wf_cliprdr_file_group_descriptor_size_valid(offsetof(FILEGROUPDESCRIPTORW, fgd), 0),
|
||||
FALSE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_descriptor_size_accepts_max_stream_count)
|
||||
{
|
||||
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid(
|
||||
descriptor_size(WF_CLIPRDR_MAX_STREAMS), WF_CLIPRDR_MAX_STREAMS),
|
||||
TRUE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_descriptor_size_rejects_stream_count_above_limit)
|
||||
{
|
||||
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid(
|
||||
descriptor_size(WF_CLIPRDR_MAX_STREAMS), WF_CLIPRDR_MAX_STREAMS + 1),
|
||||
FALSE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_descriptor_size_rejects_truncated_descriptor_array)
|
||||
{
|
||||
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid(descriptor_size(2) - 1, 2),
|
||||
FALSE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_descriptor_size_rejects_extreme_count)
|
||||
{
|
||||
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid((SIZE_T)-1, (UINT)-1),
|
||||
FALSE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
Suite *wf_cliprdr_invariant_suite(void)
|
||||
{
|
||||
Suite *s;
|
||||
TCase *tc_core;
|
||||
|
||||
s = suite_create("wf_cliprdr_invariants");
|
||||
tc_core = tcase_create("descriptor_size");
|
||||
|
||||
tcase_add_test(tc_core, test_descriptor_size_rejects_buffer_smaller_than_header);
|
||||
tcase_add_test(tc_core, test_descriptor_size_rejects_zero_items);
|
||||
tcase_add_test(tc_core, test_descriptor_size_accepts_max_stream_count);
|
||||
tcase_add_test(tc_core, test_descriptor_size_rejects_stream_count_above_limit);
|
||||
tcase_add_test(tc_core, test_descriptor_size_rejects_truncated_descriptor_array);
|
||||
tcase_add_test(tc_core, test_descriptor_size_rejects_extreme_count);
|
||||
|
||||
suite_add_tcase(s, tc_core);
|
||||
return s;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int number_failed;
|
||||
Suite *s;
|
||||
SRunner *sr;
|
||||
|
||||
s = wf_cliprdr_invariant_suite();
|
||||
sr = srunner_create(s);
|
||||
|
||||
srunner_run_all(sr, CK_NORMAL);
|
||||
number_failed = srunner_ntests_failed(sr);
|
||||
srunner_free(sr);
|
||||
|
||||
return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
@@ -8,6 +8,11 @@
|
||||
"name": "aom",
|
||||
"host": false
|
||||
},
|
||||
{
|
||||
"name": "svt-av1",
|
||||
"host": false,
|
||||
"platform": "!x86 & !arm32 & !uwp"
|
||||
},
|
||||
{
|
||||
"name": "cpu-features",
|
||||
"platform": "android"
|
||||
@@ -95,6 +100,9 @@
|
||||
},
|
||||
"overlay-ports": [
|
||||
"./res/vcpkg"
|
||||
],
|
||||
"overlay-triplets": [
|
||||
"./res/vcpkg-triplets"
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
|
||||
Reference in New Issue
Block a user