mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-07-07 23:05:08 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32345f93bf | |||
| 193e4bd5bf | |||
| 53edf3d57e | |||
| 895e297771 | |||
| 8f3959e6c6 | |||
| 5ff6b601c7 |
+65
-49
@@ -15,6 +15,7 @@ APP_BUNDLE="${APP_NAME_UPPER}.app"
|
|||||||
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
||||||
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
||||||
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
|
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
|
||||||
|
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
|
||||||
|
|
||||||
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
|
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
|
||||||
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
||||||
@@ -73,67 +74,82 @@ cat > "${CONTENTS_DIR}/Info.plist" <<EOF
|
|||||||
</plist>
|
</plist>
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
|
||||||
|
find "${APP_BUNDLE}" -name '._*' -delete
|
||||||
|
|
||||||
echo ".app created successfully."
|
echo ".app created successfully."
|
||||||
|
|
||||||
|
mkdir -p build_pkg_scripts
|
||||||
|
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
|
||||||
|
chmod +x build_pkg_scripts/postinstall
|
||||||
|
|
||||||
|
mkdir -p build_pkg_resources
|
||||||
|
cat > build_pkg_resources/welcome.html <<EOF
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #222;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
|
||||||
|
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
|
||||||
|
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
|
||||||
|
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
|
||||||
|
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
EOF
|
||||||
|
|
||||||
echo "building pkg..."
|
echo "building pkg..."
|
||||||
pkgbuild \
|
pkgbuild \
|
||||||
--identifier "${IDENTIFIER}" \
|
--identifier "${IDENTIFIER}" \
|
||||||
--version "${APP_VERSION}" \
|
--version "${APP_VERSION}" \
|
||||||
--install-location "/Applications" \
|
--install-location "/Applications" \
|
||||||
--component "${APP_BUNDLE}" \
|
--component "${APP_BUNDLE}" \
|
||||||
|
--scripts build_pkg_scripts \
|
||||||
build_pkg_temp/${APP_NAME}-component.pkg
|
build_pkg_temp/${APP_NAME}-component.pkg
|
||||||
|
|
||||||
mkdir -p build_pkg_scripts
|
cat > build_pkg_temp/Distribution <<EOF
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
cat > build_pkg_scripts/postinstall <<'EOF'
|
<installer-gui-script minSpecVersion="1">
|
||||||
#!/bin/bash
|
<title>${INSTALLER_TITLE}</title>
|
||||||
set -e
|
<welcome file="welcome.html" mime-type="text/html"/>
|
||||||
|
<options customize="never" require-scripts="false" hostArchitectures="arm64"/>
|
||||||
IDENTIFIER="cn.crossdesk.app"
|
<choices-outline>
|
||||||
|
<line choice="default">
|
||||||
# 获取当前登录用户
|
<line choice="${IDENTIFIER}"/>
|
||||||
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
|
</line>
|
||||||
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' )
|
</choices-outline>
|
||||||
|
<choice id="default" title="${INSTALLER_TITLE}"/>
|
||||||
# 清除应用的权限授权,以便重新授权
|
<choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
|
||||||
# 使用 tccutil 重置录屏权限和辅助功能权限
|
<pkg-ref id="${IDENTIFIER}"/>
|
||||||
if command -v tccutil >/dev/null 2>&1; then
|
</choice>
|
||||||
# 重置录屏权限
|
<pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
|
||||||
tccutil reset ScreenCapture "$IDENTIFIER" 2>/dev/null || true
|
</installer-gui-script>
|
||||||
# 重置辅助功能权限
|
|
||||||
tccutil reset Accessibility "$IDENTIFIER" 2>/dev/null || true
|
|
||||||
# 重置摄像头权限(如果需要)
|
|
||||||
tccutil reset Camera "$IDENTIFIER" 2>/dev/null || true
|
|
||||||
# 重置麦克风权限(如果需要)
|
|
||||||
tccutil reset Microphone "$IDENTIFIER" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 为所有用户清除权限(可选,如果需要)
|
|
||||||
# 遍历所有用户目录并清除权限
|
|
||||||
for USER_DIR in /Users/*; do
|
|
||||||
if [ -d "$USER_DIR" ] && [ "$USER_DIR" != "/Users/Shared" ]; then
|
|
||||||
USER_NAME=$(basename "$USER_DIR")
|
|
||||||
# 跳过系统用户
|
|
||||||
if [ "$USER_NAME" != "Shared" ] && [ -d "$USER_DIR/Library" ]; then
|
|
||||||
# 删除 TCC 数据库中的相关条目(需要管理员权限)
|
|
||||||
TCC_DB="$USER_DIR/Library/Application Support/com.apple.TCC/TCC.db"
|
|
||||||
if [ -f "$TCC_DB" ]; then
|
|
||||||
# 使用 sqlite3 删除相关权限记录(如果可用)
|
|
||||||
if command -v sqlite3 >/dev/null 2>&1; then
|
|
||||||
sqlite3 "$TCC_DB" "DELETE FROM access WHERE client='$IDENTIFIER' AND service IN ('kTCCServiceScreenCapture', 'kTCCServiceAccessibility');" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod +x build_pkg_scripts/postinstall
|
|
||||||
|
|
||||||
productbuild \
|
productbuild \
|
||||||
--package build_pkg_temp/${APP_NAME}-component.pkg \
|
--distribution build_pkg_temp/Distribution \
|
||||||
|
--package-path build_pkg_temp \
|
||||||
|
--resources build_pkg_resources \
|
||||||
"${PKG_NAME}"
|
"${PKG_NAME}"
|
||||||
|
|
||||||
echo "PKG package created: ${PKG_NAME}"
|
echo "PKG package created: ${PKG_NAME}"
|
||||||
@@ -171,7 +187,7 @@ APPLESCRIPT
|
|||||||
fi
|
fi
|
||||||
echo "Set icon finished"
|
echo "Set icon finished"
|
||||||
|
|
||||||
rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE}
|
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
|
||||||
|
|
||||||
echo "PKG package created successfully."
|
echo "PKG package created successfully."
|
||||||
echo "package ${APP_BUNDLE}"
|
echo "package ${APP_BUNDLE}"
|
||||||
|
|||||||
+65
-49
@@ -15,6 +15,7 @@ APP_BUNDLE="${APP_NAME_UPPER}.app"
|
|||||||
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
||||||
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
||||||
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
|
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
|
||||||
|
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
|
||||||
|
|
||||||
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
|
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
|
||||||
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
||||||
@@ -73,67 +74,82 @@ cat > "${CONTENTS_DIR}/Info.plist" <<EOF
|
|||||||
</plist>
|
</plist>
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
|
||||||
|
find "${APP_BUNDLE}" -name '._*' -delete
|
||||||
|
|
||||||
echo ".app created successfully."
|
echo ".app created successfully."
|
||||||
|
|
||||||
|
mkdir -p build_pkg_scripts
|
||||||
|
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
|
||||||
|
chmod +x build_pkg_scripts/postinstall
|
||||||
|
|
||||||
|
mkdir -p build_pkg_resources
|
||||||
|
cat > build_pkg_resources/welcome.html <<EOF
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #222;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
|
||||||
|
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
|
||||||
|
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
|
||||||
|
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
|
||||||
|
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
EOF
|
||||||
|
|
||||||
echo "building pkg..."
|
echo "building pkg..."
|
||||||
pkgbuild \
|
pkgbuild \
|
||||||
--identifier "${IDENTIFIER}" \
|
--identifier "${IDENTIFIER}" \
|
||||||
--version "${APP_VERSION}" \
|
--version "${APP_VERSION}" \
|
||||||
--install-location "/Applications" \
|
--install-location "/Applications" \
|
||||||
--component "${APP_BUNDLE}" \
|
--component "${APP_BUNDLE}" \
|
||||||
|
--scripts build_pkg_scripts \
|
||||||
build_pkg_temp/${APP_NAME}-component.pkg
|
build_pkg_temp/${APP_NAME}-component.pkg
|
||||||
|
|
||||||
mkdir -p build_pkg_scripts
|
cat > build_pkg_temp/Distribution <<EOF
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
cat > build_pkg_scripts/postinstall <<'EOF'
|
<installer-gui-script minSpecVersion="1">
|
||||||
#!/bin/bash
|
<title>${INSTALLER_TITLE}</title>
|
||||||
set -e
|
<welcome file="welcome.html" mime-type="text/html"/>
|
||||||
|
<options customize="never" require-scripts="false" hostArchitectures="x86_64"/>
|
||||||
IDENTIFIER="cn.crossdesk.app"
|
<choices-outline>
|
||||||
|
<line choice="default">
|
||||||
# 获取当前登录用户
|
<line choice="${IDENTIFIER}"/>
|
||||||
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
|
</line>
|
||||||
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' )
|
</choices-outline>
|
||||||
|
<choice id="default" title="${INSTALLER_TITLE}"/>
|
||||||
# 清除应用的权限授权,以便重新授权
|
<choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
|
||||||
# 使用 tccutil 重置录屏权限和辅助功能权限
|
<pkg-ref id="${IDENTIFIER}"/>
|
||||||
if command -v tccutil >/dev/null 2>&1; then
|
</choice>
|
||||||
# 重置录屏权限
|
<pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
|
||||||
tccutil reset ScreenCapture "$IDENTIFIER" 2>/dev/null || true
|
</installer-gui-script>
|
||||||
# 重置辅助功能权限
|
|
||||||
tccutil reset Accessibility "$IDENTIFIER" 2>/dev/null || true
|
|
||||||
# 重置摄像头权限(如果需要)
|
|
||||||
tccutil reset Camera "$IDENTIFIER" 2>/dev/null || true
|
|
||||||
# 重置麦克风权限(如果需要)
|
|
||||||
tccutil reset Microphone "$IDENTIFIER" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 为所有用户清除权限(可选,如果需要)
|
|
||||||
# 遍历所有用户目录并清除权限
|
|
||||||
for USER_DIR in /Users/*; do
|
|
||||||
if [ -d "$USER_DIR" ] && [ "$USER_DIR" != "/Users/Shared" ]; then
|
|
||||||
USER_NAME=$(basename "$USER_DIR")
|
|
||||||
# 跳过系统用户
|
|
||||||
if [ "$USER_NAME" != "Shared" ] && [ -d "$USER_DIR/Library" ]; then
|
|
||||||
# 删除 TCC 数据库中的相关条目(需要管理员权限)
|
|
||||||
TCC_DB="$USER_DIR/Library/Application Support/com.apple.TCC/TCC.db"
|
|
||||||
if [ -f "$TCC_DB" ]; then
|
|
||||||
# 使用 sqlite3 删除相关权限记录(如果可用)
|
|
||||||
if command -v sqlite3 >/dev/null 2>&1; then
|
|
||||||
sqlite3 "$TCC_DB" "DELETE FROM access WHERE client='$IDENTIFIER' AND service IN ('kTCCServiceScreenCapture', 'kTCCServiceAccessibility');" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod +x build_pkg_scripts/postinstall
|
|
||||||
|
|
||||||
productbuild \
|
productbuild \
|
||||||
--package build_pkg_temp/${APP_NAME}-component.pkg \
|
--distribution build_pkg_temp/Distribution \
|
||||||
|
--package-path build_pkg_temp \
|
||||||
|
--resources build_pkg_resources \
|
||||||
"${PKG_NAME}"
|
"${PKG_NAME}"
|
||||||
|
|
||||||
echo "PKG package created: ${PKG_NAME}"
|
echo "PKG package created: ${PKG_NAME}"
|
||||||
@@ -171,7 +187,7 @@ APPLESCRIPT
|
|||||||
fi
|
fi
|
||||||
echo "Set icon finished"
|
echo "Set icon finished"
|
||||||
|
|
||||||
rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE}
|
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
|
||||||
|
|
||||||
echo "PKG package created successfully."
|
echo "PKG package created successfully."
|
||||||
echo "package ${APP_BUNDLE}"
|
echo "package ${APP_BUNDLE}"
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
APP_IDENTIFIER="cn.crossdesk.app"
|
||||||
|
|
||||||
|
# Keep known historical identifiers here. tccutil only resets identifiers that
|
||||||
|
# Launch Services can currently resolve, so path/db cleanup below remains a
|
||||||
|
# best-effort fallback for stale entries from unsigned or removed builds.
|
||||||
|
BUNDLE_IDENTIFIERS=(
|
||||||
|
"cn.crossdesk.app"
|
||||||
|
"cn.crossdesk.CrossDesk"
|
||||||
|
"com.crossdesk.app"
|
||||||
|
"com.crossdesk.CrossDesk"
|
||||||
|
"com.kunkundi.crossdesk"
|
||||||
|
"com.kunkundi.CrossDesk"
|
||||||
|
)
|
||||||
|
|
||||||
|
TCC_SERVICES=(
|
||||||
|
"ScreenCapture"
|
||||||
|
"Accessibility"
|
||||||
|
"Microphone"
|
||||||
|
"AudioCapture"
|
||||||
|
)
|
||||||
|
|
||||||
|
run_tccutil() {
|
||||||
|
local user_name="$1"
|
||||||
|
local user_id="$2"
|
||||||
|
local service="$3"
|
||||||
|
local bundle_id="$4"
|
||||||
|
|
||||||
|
if [ -n "$user_name" ] && [ -n "$user_id" ]; then
|
||||||
|
/bin/launchctl asuser "$user_id" \
|
||||||
|
/usr/bin/sudo -u "$user_name" \
|
||||||
|
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
|
||||||
|
else
|
||||||
|
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_bundle_tcc() {
|
||||||
|
local user_name="$1"
|
||||||
|
local user_id="$2"
|
||||||
|
local bundle_id
|
||||||
|
local service
|
||||||
|
|
||||||
|
for bundle_id in "${BUNDLE_IDENTIFIERS[@]}"; do
|
||||||
|
if run_tccutil "$user_name" "$user_id" "All" "$bundle_id"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
for service in "${TCC_SERVICES[@]}"; do
|
||||||
|
run_tccutil "$user_name" "$user_id" "$service" "$bundle_id" || true
|
||||||
|
done
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_tcc_db() {
|
||||||
|
local db_path="$1"
|
||||||
|
|
||||||
|
if [ ! -f "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
/usr/bin/sqlite3 "$db_path" <<'SQL' >/dev/null 2>&1 || true
|
||||||
|
DELETE FROM access
|
||||||
|
WHERE service IN (
|
||||||
|
'kTCCServiceScreenCapture',
|
||||||
|
'kTCCServiceAccessibility',
|
||||||
|
'kTCCServiceMicrophone',
|
||||||
|
'kTCCServiceAudioCapture'
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
client IN (
|
||||||
|
'cn.crossdesk.app',
|
||||||
|
'cn.crossdesk.CrossDesk',
|
||||||
|
'com.crossdesk.app',
|
||||||
|
'com.crossdesk.CrossDesk',
|
||||||
|
'com.kunkundi.crossdesk',
|
||||||
|
'com.kunkundi.CrossDesk'
|
||||||
|
)
|
||||||
|
OR lower(client) LIKE '%crossdesk%'
|
||||||
|
);
|
||||||
|
SQL
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_user_tcc_db() {
|
||||||
|
local user_name="$1"
|
||||||
|
local home_dir
|
||||||
|
|
||||||
|
home_dir=$(/usr/bin/dscl . -read "/Users/${user_name}" NFSHomeDirectory 2>/dev/null |
|
||||||
|
/usr/bin/awk '{print $2}')
|
||||||
|
if [ -z "$home_dir" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup_tcc_db "${home_dir}/Library/Application Support/com.apple.TCC/TCC.db"
|
||||||
|
}
|
||||||
|
|
||||||
|
CONSOLE_USER=$(/usr/bin/stat -f "%Su" /dev/console 2>/dev/null || true)
|
||||||
|
if [ -n "$CONSOLE_USER" ] &&
|
||||||
|
[ "$CONSOLE_USER" != "root" ] &&
|
||||||
|
[ "$CONSOLE_USER" != "loginwindow" ]; then
|
||||||
|
CONSOLE_UID=$(/usr/bin/id -u "$CONSOLE_USER" 2>/dev/null || true)
|
||||||
|
reset_bundle_tcc "$CONSOLE_USER" "$CONSOLE_UID"
|
||||||
|
cleanup_user_tcc_db "$CONSOLE_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Also clear any system/root-scoped decisions as a harmless fallback.
|
||||||
|
reset_bundle_tcc "" ""
|
||||||
|
cleanup_tcc_db "/Library/Application Support/com.apple.TCC/TCC.db"
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -310,6 +310,10 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
|||||||
if (IsFunctionKey(cg_key_code) && !is_down) {
|
if (IsFunctionKey(cg_key_code) && !is_down) {
|
||||||
CGEventRef fn_release_event =
|
CGEventRef fn_release_event =
|
||||||
CGEventCreateKeyboardEvent(NULL, fn_key_code_, false);
|
CGEventCreateKeyboardEvent(NULL, fn_key_code_, false);
|
||||||
|
if (!fn_release_event) {
|
||||||
|
LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
CGEventPost(kCGHIDEventTap, fn_release_event);
|
CGEventPost(kCGHIDEventTap, fn_release_event);
|
||||||
CFRelease(fn_release_event);
|
CFRelease(fn_release_event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "mouse_controller.h"
|
#include "mouse_controller.h"
|
||||||
|
|
||||||
#include <ApplicationServices/ApplicationServices.h>
|
#include <ApplicationServices/ApplicationServices.h>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
|
|
||||||
@@ -20,83 +21,99 @@ int MouseController::Destroy() { return 0; }
|
|||||||
|
|
||||||
int MouseController::SendMouseCommand(RemoteAction remote_action,
|
int MouseController::SendMouseCommand(RemoteAction remote_action,
|
||||||
int display_index) {
|
int display_index) {
|
||||||
|
if (remote_action.type != ControlType::mouse) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (display_index < 0 ||
|
||||||
|
display_index >= static_cast<int>(display_info_list_.size())) {
|
||||||
|
LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}",
|
||||||
|
display_index, display_info_list_.size());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DisplayInfo& display_info = display_info_list_[display_index];
|
||||||
|
if (display_info.width <= 0 || display_info.height <= 0) {
|
||||||
|
LOG_WARN("Mouse command skipped, invalid display geometry: {}x{}",
|
||||||
|
display_info.width, display_info.height);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float normalized_x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
|
||||||
|
const float normalized_y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
|
||||||
int mouse_pos_x =
|
int mouse_pos_x =
|
||||||
remote_action.m.x * display_info_list_[display_index].width +
|
normalized_x * display_info.width + display_info.left;
|
||||||
display_info_list_[display_index].left;
|
|
||||||
int mouse_pos_y =
|
int mouse_pos_y =
|
||||||
remote_action.m.y * display_info_list_[display_index].height +
|
normalized_y * display_info.height + display_info.top;
|
||||||
display_info_list_[display_index].top;
|
|
||||||
|
|
||||||
if (remote_action.type == ControlType::mouse) {
|
CGEventRef mouse_event = nullptr;
|
||||||
CGEventRef mouse_event = nullptr;
|
CGEventType mouse_type;
|
||||||
CGEventType mouse_type;
|
CGMouseButton mouse_button;
|
||||||
CGMouseButton mouse_button;
|
CGPoint mouse_point = CGPointMake(mouse_pos_x, mouse_pos_y);
|
||||||
CGPoint mouse_point = CGPointMake(mouse_pos_x, mouse_pos_y);
|
|
||||||
|
|
||||||
switch (remote_action.m.flag) {
|
switch (remote_action.m.flag) {
|
||||||
case MouseFlag::left_down:
|
case MouseFlag::left_down:
|
||||||
mouse_type = kCGEventLeftMouseDown;
|
mouse_type = kCGEventLeftMouseDown;
|
||||||
left_dragging_ = true;
|
left_dragging_ = true;
|
||||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||||
kCGMouseButtonLeft);
|
kCGMouseButtonLeft);
|
||||||
break;
|
break;
|
||||||
case MouseFlag::left_up:
|
case MouseFlag::left_up:
|
||||||
mouse_type = kCGEventLeftMouseUp;
|
mouse_type = kCGEventLeftMouseUp;
|
||||||
left_dragging_ = false;
|
left_dragging_ = false;
|
||||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||||
kCGMouseButtonLeft);
|
kCGMouseButtonLeft);
|
||||||
break;
|
break;
|
||||||
case MouseFlag::right_down:
|
case MouseFlag::right_down:
|
||||||
mouse_type = kCGEventRightMouseDown;
|
mouse_type = kCGEventRightMouseDown;
|
||||||
right_dragging_ = true;
|
right_dragging_ = true;
|
||||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||||
kCGMouseButtonRight);
|
kCGMouseButtonRight);
|
||||||
break;
|
break;
|
||||||
case MouseFlag::right_up:
|
case MouseFlag::right_up:
|
||||||
mouse_type = kCGEventRightMouseUp;
|
mouse_type = kCGEventRightMouseUp;
|
||||||
right_dragging_ = false;
|
right_dragging_ = false;
|
||||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||||
kCGMouseButtonRight);
|
kCGMouseButtonRight);
|
||||||
break;
|
break;
|
||||||
case MouseFlag::middle_down:
|
case MouseFlag::middle_down:
|
||||||
mouse_type = kCGEventOtherMouseDown;
|
mouse_type = kCGEventOtherMouseDown;
|
||||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||||
kCGMouseButtonCenter);
|
kCGMouseButtonCenter);
|
||||||
break;
|
break;
|
||||||
case MouseFlag::middle_up:
|
case MouseFlag::middle_up:
|
||||||
mouse_type = kCGEventOtherMouseUp;
|
mouse_type = kCGEventOtherMouseUp;
|
||||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||||
kCGMouseButtonCenter);
|
kCGMouseButtonCenter);
|
||||||
break;
|
break;
|
||||||
case MouseFlag::wheel_vertical:
|
case MouseFlag::wheel_vertical:
|
||||||
mouse_event = CGEventCreateScrollWheelEvent(
|
mouse_event = CGEventCreateScrollWheelEvent(
|
||||||
NULL, kCGScrollEventUnitLine, 2, remote_action.m.s, 0);
|
NULL, kCGScrollEventUnitLine, 2, remote_action.m.s, 0);
|
||||||
break;
|
break;
|
||||||
case MouseFlag::wheel_horizontal:
|
case MouseFlag::wheel_horizontal:
|
||||||
mouse_event = CGEventCreateScrollWheelEvent(
|
mouse_event = CGEventCreateScrollWheelEvent(
|
||||||
NULL, kCGScrollEventUnitLine, 2, 0, remote_action.m.s);
|
NULL, kCGScrollEventUnitLine, 2, 0, remote_action.m.s);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (left_dragging_) {
|
if (left_dragging_) {
|
||||||
mouse_type = kCGEventLeftMouseDragged;
|
mouse_type = kCGEventLeftMouseDragged;
|
||||||
mouse_button = kCGMouseButtonLeft;
|
mouse_button = kCGMouseButtonLeft;
|
||||||
} else if (right_dragging_) {
|
} else if (right_dragging_) {
|
||||||
mouse_type = kCGEventRightMouseDragged;
|
mouse_type = kCGEventRightMouseDragged;
|
||||||
mouse_button = kCGMouseButtonRight;
|
mouse_button = kCGMouseButtonRight;
|
||||||
} else {
|
} else {
|
||||||
mouse_type = kCGEventMouseMoved;
|
mouse_type = kCGEventMouseMoved;
|
||||||
mouse_button = kCGMouseButtonLeft;
|
mouse_button = kCGMouseButtonLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||||
mouse_button);
|
mouse_button);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mouse_event) {
|
if (mouse_event) {
|
||||||
CGEventPost(kCGHIDEventTap, mouse_event);
|
CGEventPost(kCGHIDEventTap, mouse_event);
|
||||||
CFRelease(mouse_event);
|
CFRelease(mouse_event);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
+35
-4
@@ -636,6 +636,12 @@ int Render::LoadSettingsFromCacheFile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int Render::ScreenCapturerInit() {
|
int Render::ScreenCapturerInit() {
|
||||||
|
#ifdef __APPLE__
|
||||||
|
if (!EnsureMacScreenRecordingPermission()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (!screen_capturer_) {
|
if (!screen_capturer_) {
|
||||||
screen_capturer_ = (ScreenCapturer*)screen_capturer_factory_->Create();
|
screen_capturer_ = (ScreenCapturer*)screen_capturer_factory_->Create();
|
||||||
}
|
}
|
||||||
@@ -705,6 +711,12 @@ int Render::ScreenCapturerInit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int Render::StartScreenCapturer() {
|
int Render::StartScreenCapturer() {
|
||||||
|
#ifdef __APPLE__
|
||||||
|
if (!EnsureMacScreenRecordingPermission()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (!screen_capturer_) {
|
if (!screen_capturer_) {
|
||||||
LOG_INFO("Screen capturer instance missing, recreating before start");
|
LOG_INFO("Screen capturer instance missing, recreating before start");
|
||||||
if (0 != ScreenCapturerInit()) {
|
if (0 != ScreenCapturerInit()) {
|
||||||
@@ -752,11 +764,16 @@ int Render::StartSpeakerCapturer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (speaker_capturer_) {
|
if (speaker_capturer_) {
|
||||||
speaker_capturer_->Start();
|
const int ret = speaker_capturer_->Start();
|
||||||
|
if (ret != 0) {
|
||||||
|
LOG_ERROR("Start speaker capturer failed: {}", ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
start_speaker_capturer_ = true;
|
start_speaker_capturer_ = true;
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Render::StopSpeakerCapturer() {
|
int Render::StopSpeakerCapturer() {
|
||||||
@@ -769,6 +786,12 @@ int Render::StopSpeakerCapturer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int Render::StartMouseController() {
|
int Render::StartMouseController() {
|
||||||
|
#ifdef __APPLE__
|
||||||
|
if (!EnsureMacAccessibilityPermission()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (!device_controller_factory_) {
|
if (!device_controller_factory_) {
|
||||||
LOG_INFO("Device controller factory is nullptr");
|
LOG_INFO("Device controller factory is nullptr");
|
||||||
return -1;
|
return -1;
|
||||||
@@ -826,6 +849,13 @@ int Render::StopMouseController() {
|
|||||||
int Render::StartKeyboardCapturer() {
|
int Render::StartKeyboardCapturer() {
|
||||||
keyboard_capturer_uses_sdl_events_ = false;
|
keyboard_capturer_uses_sdl_events_ = false;
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
if (!EnsureMacAccessibilityPermission()) {
|
||||||
|
keyboard_capturer_uses_sdl_events_ = true;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined(__linux__) && !defined(__APPLE__)
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
if (IsWaylandSession()) {
|
if (IsWaylandSession()) {
|
||||||
keyboard_capturer_uses_sdl_events_ = true;
|
keyboard_capturer_uses_sdl_events_ = true;
|
||||||
@@ -1124,8 +1154,9 @@ void Render::UpdateInteractions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (start_speaker_capturer_ && !speaker_capturer_is_started_) {
|
if (start_speaker_capturer_ && !speaker_capturer_is_started_) {
|
||||||
StartSpeakerCapturer();
|
if (0 == StartSpeakerCapturer()) {
|
||||||
speaker_capturer_is_started_ = true;
|
speaker_capturer_is_started_ = true;
|
||||||
|
}
|
||||||
} else if (!start_speaker_capturer_ && speaker_capturer_is_started_) {
|
} else if (!start_speaker_capturer_ && speaker_capturer_is_started_) {
|
||||||
StopSpeakerCapturer();
|
StopSpeakerCapturer();
|
||||||
speaker_capturer_is_started_ = false;
|
speaker_capturer_is_started_ = false;
|
||||||
|
|||||||
+14
-5
@@ -296,6 +296,9 @@ class Render {
|
|||||||
void OpenScreenRecordingPreferences();
|
void OpenScreenRecordingPreferences();
|
||||||
void OpenAccessibilityPreferences();
|
void OpenAccessibilityPreferences();
|
||||||
bool DrawToggleSwitch(const char* id, bool active, bool enabled);
|
bool DrawToggleSwitch(const char* id, bool active, bool enabled);
|
||||||
|
void RefreshMacPermissionStatus(bool force);
|
||||||
|
bool EnsureMacScreenRecordingPermission();
|
||||||
|
bool EnsureMacAccessibilityPermission();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -550,8 +553,8 @@ class Render {
|
|||||||
std::string remote_client_id_ = "";
|
std::string remote_client_id_ = "";
|
||||||
std::unordered_set<int> pressed_keyboard_keys_;
|
std::unordered_set<int> pressed_keyboard_keys_;
|
||||||
std::mutex pressed_keyboard_keys_mutex_;
|
std::mutex pressed_keyboard_keys_mutex_;
|
||||||
SDL_Event last_mouse_event;
|
SDL_Event last_mouse_event{};
|
||||||
SDL_AudioStream* output_stream_;
|
SDL_AudioStream* output_stream_ = nullptr;
|
||||||
uint32_t STREAM_REFRESH_EVENT = 0;
|
uint32_t STREAM_REFRESH_EVENT = 0;
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
std::atomic<bool> pending_windows_service_sas_{false};
|
std::atomic<bool> pending_windows_service_sas_{false};
|
||||||
@@ -681,8 +684,8 @@ class Render {
|
|||||||
// Map file_id to FileTransferState for global file transfer (props == null)
|
// Map file_id to FileTransferState for global file transfer (props == null)
|
||||||
std::unordered_map<uint32_t, FileTransferState*> file_id_to_transfer_state_;
|
std::unordered_map<uint32_t, FileTransferState*> file_id_to_transfer_state_;
|
||||||
std::shared_mutex file_id_to_transfer_state_mutex_;
|
std::shared_mutex file_id_to_transfer_state_mutex_;
|
||||||
SDL_AudioDeviceID input_dev_;
|
SDL_AudioDeviceID input_dev_ = 0;
|
||||||
SDL_AudioDeviceID output_dev_;
|
SDL_AudioDeviceID output_dev_ = 0;
|
||||||
ScreenCapturerFactory* screen_capturer_factory_ = nullptr;
|
ScreenCapturerFactory* screen_capturer_factory_ = nullptr;
|
||||||
ScreenCapturer* screen_capturer_ = nullptr;
|
ScreenCapturer* screen_capturer_ = nullptr;
|
||||||
SpeakerCapturerFactory* speaker_capturer_factory_ = nullptr;
|
SpeakerCapturerFactory* speaker_capturer_factory_ = nullptr;
|
||||||
@@ -691,7 +694,7 @@ class Render {
|
|||||||
MouseController* mouse_controller_ = nullptr;
|
MouseController* mouse_controller_ = nullptr;
|
||||||
KeyboardCapturer* keyboard_capturer_ = nullptr;
|
KeyboardCapturer* keyboard_capturer_ = nullptr;
|
||||||
std::vector<DisplayInfo> display_info_list_;
|
std::vector<DisplayInfo> display_info_list_;
|
||||||
uint64_t last_frame_time_;
|
uint64_t last_frame_time_ = 0;
|
||||||
std::string last_video_frame_stream_id_;
|
std::string last_video_frame_stream_id_;
|
||||||
bool show_new_version_icon_ = false;
|
bool show_new_version_icon_ = false;
|
||||||
bool show_new_version_icon_in_menu_ = true;
|
bool show_new_version_icon_in_menu_ = true;
|
||||||
@@ -699,6 +702,12 @@ class Render {
|
|||||||
double new_version_icon_render_start_time_ = 0.0;
|
double new_version_icon_render_start_time_ = 0.0;
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
bool show_request_permission_window_ = true;
|
bool show_request_permission_window_ = true;
|
||||||
|
bool mac_permission_status_initialized_ = false;
|
||||||
|
uint32_t mac_permission_last_check_tick_ = 0;
|
||||||
|
bool mac_screen_recording_permission_granted_ = false;
|
||||||
|
bool mac_accessibility_permission_granted_ = false;
|
||||||
|
bool mac_screen_recording_permission_requested_ = false;
|
||||||
|
bool mac_accessibility_permission_requested_ = false;
|
||||||
#endif
|
#endif
|
||||||
char client_id_[10] = "";
|
char client_id_[10] = "";
|
||||||
char client_id_display_[12] = "";
|
char client_id_display_[12] = "";
|
||||||
|
|||||||
@@ -1196,8 +1196,13 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
|||||||
remote_action.k.extended);
|
remote_action.k.extended);
|
||||||
} else if (remote_action.type == ControlType::display_id &&
|
} else if (remote_action.type == ControlType::display_id &&
|
||||||
render->screen_capturer_) {
|
render->screen_capturer_) {
|
||||||
render->selected_display_ = remote_action.d;
|
const int ret = render->screen_capturer_->SwitchTo(remote_action.d);
|
||||||
render->screen_capturer_->SwitchTo(remote_action.d);
|
if (ret == 0) {
|
||||||
|
render->selected_display_ = remote_action.d;
|
||||||
|
} else {
|
||||||
|
LOG_WARN("Display switch skipped, invalid display_id={}",
|
||||||
|
remote_action.d);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,27 @@
|
|||||||
#include <ApplicationServices/ApplicationServices.h>
|
#include <ApplicationServices/ApplicationServices.h>
|
||||||
#include <CoreGraphics/CoreGraphics.h>
|
#include <CoreGraphics/CoreGraphics.h>
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#include <unistd.h>
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr uint32_t kPermissionRefreshIntervalVisibleMs = 500;
|
||||||
|
|
||||||
|
void OpenPrivacyPreferences(const char* pane) {
|
||||||
|
if (pane == nullptr || pane[0] == '\0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string command =
|
||||||
|
"open \"x-apple.systempreferences:com.apple.preference.security?";
|
||||||
|
command += pane;
|
||||||
|
command += "\"";
|
||||||
|
system(command.c_str());
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
|
bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
|
||||||
const float TRACK_HEIGHT = ImGui::GetFrameHeight();
|
const float TRACK_HEIGHT = ImGui::GetFrameHeight();
|
||||||
const float TRACK_WIDTH = TRACK_HEIGHT * 1.8f;
|
const float TRACK_WIDTH = TRACK_HEIGHT * 1.8f;
|
||||||
@@ -35,16 +51,19 @@ bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
|
|||||||
bool hovered = ImGui::IsItemHovered();
|
bool hovered = ImGui::IsItemHovered();
|
||||||
bool clicked = ImGui::IsItemClicked() && enabled;
|
bool clicked = ImGui::IsItemClicked() && enabled;
|
||||||
|
|
||||||
ImVec4 track_color = active ? (hovered && enabled ? COLOR_ACTIVE_HOVER : COLOR_ACTIVE)
|
ImVec4 track_color =
|
||||||
: (hovered && enabled ? COLOR_INACTIVE_HOVER : COLOR_INACTIVE);
|
active ? (hovered && enabled ? COLOR_ACTIVE_HOVER : COLOR_ACTIVE)
|
||||||
|
: (hovered && enabled ? COLOR_INACTIVE_HOVER : COLOR_INACTIVE);
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
track_color.w *= DISABLED_ALPHA;
|
track_color.w *= DISABLED_ALPHA;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImVec2 track_min = ImVec2(track_pos.x, track_pos.y + 0.5f);
|
ImVec2 track_min = ImVec2(track_pos.x, track_pos.y + 0.5f);
|
||||||
ImVec2 track_max = ImVec2(track_pos.x + TRACK_WIDTH, track_pos.y + TRACK_HEIGHT - 0.5f);
|
ImVec2 track_max = ImVec2(track_pos.x + TRACK_WIDTH,
|
||||||
draw_list->AddRectFilled(track_min, track_max, ImGui::GetColorU32(track_color), TRACK_RADIUS);
|
track_pos.y + TRACK_HEIGHT - 0.5f);
|
||||||
|
draw_list->AddRectFilled(track_min, track_max,
|
||||||
|
ImGui::GetColorU32(track_color), TRACK_RADIUS);
|
||||||
|
|
||||||
float knob_position = active ? 1.0f : 0.0f;
|
float knob_position = active ? 1.0f : 0.0f;
|
||||||
float knob_min_x = track_pos.x + KNOB_PADDING;
|
float knob_min_x = track_pos.x + KNOB_PADDING;
|
||||||
@@ -59,7 +78,8 @@ bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
|
|||||||
|
|
||||||
ImVec2 knob_min = ImVec2(knob_x, knob_y);
|
ImVec2 knob_min = ImVec2(knob_x, knob_y);
|
||||||
ImVec2 knob_max = ImVec2(knob_x + KNOB_WIDTH, knob_y + KNOB_HEIGHT);
|
ImVec2 knob_max = ImVec2(knob_x + KNOB_WIDTH, knob_y + KNOB_HEIGHT);
|
||||||
draw_list->AddRectFilled(knob_min, knob_max, ImGui::GetColorU32(knob_color), KNOB_RADIUS);
|
draw_list->AddRectFilled(knob_min, knob_max,
|
||||||
|
ImGui::GetColorU32(knob_color), KNOB_RADIUS);
|
||||||
|
|
||||||
return clicked;
|
return clicked;
|
||||||
}
|
}
|
||||||
@@ -81,29 +101,82 @@ bool Render::CheckAccessibilityPermission() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Render::OpenAccessibilityPreferences() {
|
void Render::OpenAccessibilityPreferences() {
|
||||||
NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES};
|
if (!mac_accessibility_permission_requested_) {
|
||||||
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
|
NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES};
|
||||||
|
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
|
||||||
system("open "
|
} else {
|
||||||
"\"x-apple.systempreferences:com.apple.preference.security?Privacy_"
|
OpenPrivacyPreferences("Privacy_Accessibility");
|
||||||
"Accessibility\"");
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Render::OpenScreenRecordingPreferences() {
|
void Render::OpenScreenRecordingPreferences() {
|
||||||
if (@available(macOS 10.15, *)) {
|
if (@available(macOS 10.15, *)) {
|
||||||
CGRequestScreenCaptureAccess();
|
if (!mac_screen_recording_permission_requested_) {
|
||||||
|
CGRequestScreenCaptureAccess();
|
||||||
|
} else {
|
||||||
|
OpenPrivacyPreferences("Privacy_ScreenCapture");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
OpenPrivacyPreferences("Privacy_ScreenCapture");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Render::RefreshMacPermissionStatus(bool force) {
|
||||||
|
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||||
|
if (!force && mac_permission_status_initialized_ &&
|
||||||
|
now - mac_permission_last_check_tick_ <
|
||||||
|
kPermissionRefreshIntervalVisibleMs) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
system("open "
|
const bool old_screen_recording_granted =
|
||||||
"\"x-apple.systempreferences:com.apple.preference.security?Privacy_"
|
mac_screen_recording_permission_granted_;
|
||||||
"ScreenCapture\"");
|
const bool old_accessibility_granted = mac_accessibility_permission_granted_;
|
||||||
|
|
||||||
|
mac_screen_recording_permission_granted_ =
|
||||||
|
CheckScreenRecordingPermission();
|
||||||
|
mac_accessibility_permission_granted_ = CheckAccessibilityPermission();
|
||||||
|
mac_permission_last_check_tick_ = now;
|
||||||
|
mac_permission_status_initialized_ = true;
|
||||||
|
|
||||||
|
if (old_screen_recording_granted !=
|
||||||
|
mac_screen_recording_permission_granted_ ||
|
||||||
|
old_accessibility_granted != mac_accessibility_permission_granted_) {
|
||||||
|
LOG_INFO("macOS permission status: screen_recording={}, accessibility={}",
|
||||||
|
mac_screen_recording_permission_granted_,
|
||||||
|
mac_accessibility_permission_granted_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Render::EnsureMacScreenRecordingPermission() {
|
||||||
|
RefreshMacPermissionStatus(false);
|
||||||
|
if (mac_screen_recording_permission_granted_) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
show_request_permission_window_ = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Render::EnsureMacAccessibilityPermission() {
|
||||||
|
RefreshMacPermissionStatus(false);
|
||||||
|
if (mac_accessibility_permission_granted_) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
show_request_permission_window_ = true;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Render::RequestPermissionWindow() {
|
int Render::RequestPermissionWindow() {
|
||||||
bool screen_recording_granted = CheckScreenRecordingPermission();
|
RefreshMacPermissionStatus(false);
|
||||||
bool accessibility_granted = CheckAccessibilityPermission();
|
|
||||||
|
|
||||||
show_request_permission_window_ = !screen_recording_granted || !accessibility_granted;
|
const bool screen_recording_granted =
|
||||||
|
mac_screen_recording_permission_granted_;
|
||||||
|
const bool accessibility_granted = mac_accessibility_permission_granted_;
|
||||||
|
|
||||||
|
show_request_permission_window_ =
|
||||||
|
!screen_recording_granted || !accessibility_granted;
|
||||||
|
|
||||||
if (!show_request_permission_window_) {
|
if (!show_request_permission_window_) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -162,8 +235,10 @@ int Render::RequestPermissionWindow() {
|
|||||||
if (accessibility_granted) {
|
if (accessibility_granted) {
|
||||||
DrawToggleSwitch("accessibility_toggle_on", true, false);
|
DrawToggleSwitch("accessibility_toggle_on", true, false);
|
||||||
} else {
|
} else {
|
||||||
if (DrawToggleSwitch("accessibility_toggle", accessibility_granted, !accessibility_granted)) {
|
if (DrawToggleSwitch("accessibility_toggle", false, true)) {
|
||||||
OpenAccessibilityPreferences();
|
OpenAccessibilityPreferences();
|
||||||
|
mac_accessibility_permission_requested_ = true;
|
||||||
|
RefreshMacPermissionStatus(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,12 +253,12 @@ int Render::RequestPermissionWindow() {
|
|||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::SetCursorPosX(checkbox_padding);
|
ImGui::SetCursorPosX(checkbox_padding);
|
||||||
if (screen_recording_granted) {
|
if (screen_recording_granted) {
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 10.0f);
|
|
||||||
DrawToggleSwitch("screen_recording_toggle_on", true, false);
|
DrawToggleSwitch("screen_recording_toggle_on", true, false);
|
||||||
} else {
|
} else {
|
||||||
if (DrawToggleSwitch("screen_recording_toggle", screen_recording_granted,
|
if (DrawToggleSwitch("screen_recording_toggle", false, true)) {
|
||||||
!screen_recording_granted)) {
|
|
||||||
OpenScreenRecordingPreferences();
|
OpenScreenRecordingPreferences();
|
||||||
|
mac_screen_recording_permission_requested_ = true;
|
||||||
|
RefreshMacPermissionStatus(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,11 @@ int ScreenCapturerSck::Init(const int fps, cb_desktop_data cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
screen_capturer_sck_impl_ = CreateScreenCapturerSck();
|
screen_capturer_sck_impl_ = CreateScreenCapturerSck();
|
||||||
screen_capturer_sck_impl_->Init(fps, on_data_);
|
const int ret = screen_capturer_sck_impl_->Init(fps, on_data_);
|
||||||
|
if (ret != 0) {
|
||||||
|
screen_capturer_sck_impl_.reset();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -29,8 +33,11 @@ int ScreenCapturerSck::Destroy() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int ScreenCapturerSck::Start(bool show_cursor) {
|
int ScreenCapturerSck::Start(bool show_cursor) {
|
||||||
screen_capturer_sck_impl_->Start(show_cursor);
|
if (!screen_capturer_sck_impl_) {
|
||||||
return 0;
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return screen_capturer_sck_impl_->Start(show_cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
int ScreenCapturerSck::Stop() {
|
int ScreenCapturerSck::Stop() {
|
||||||
|
|||||||
@@ -16,7 +16,12 @@
|
|||||||
#include <IOKit/graphics/IOGraphicsLib.h>
|
#include <IOKit/graphics/IOGraphicsLib.h>
|
||||||
#include <IOSurface/IOSurface.h>
|
#include <IOSurface/IOSurface.h>
|
||||||
#include <ScreenCaptureKit/ScreenCaptureKit.h>
|
#include <ScreenCaptureKit/ScreenCaptureKit.h>
|
||||||
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstring>
|
||||||
|
#include <limits>
|
||||||
|
#include <map>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "display_info.h"
|
#include "display_info.h"
|
||||||
@@ -28,6 +33,15 @@ class ScreenCapturerSckImpl;
|
|||||||
|
|
||||||
static const int kFullDesktopScreenId = -1;
|
static const int kFullDesktopScreenId = -1;
|
||||||
|
|
||||||
|
static std::string NSErrorToString(NSError *error) {
|
||||||
|
if (!error) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *description = [error.localizedDescription UTF8String];
|
||||||
|
return description ? description : "";
|
||||||
|
}
|
||||||
|
|
||||||
// The ScreenCaptureKit API was available in macOS 12.3, but full-screen capture
|
// The ScreenCaptureKit API was available in macOS 12.3, but full-screen capture
|
||||||
// was reported to be broken before macOS 13 - see http://crbug.com/40234870.
|
// was reported to be broken before macOS 13 - see http://crbug.com/40234870.
|
||||||
// Also, the `SCContentFilter` fields `contentRect` and `pointPixelScale` were
|
// Also, the `SCContentFilter` fields `contentRect` and `pointPixelScale` were
|
||||||
@@ -78,6 +92,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
|||||||
std::map<CGDirectDisplayID, int> display_id_map_reverse_;
|
std::map<CGDirectDisplayID, int> display_id_map_reverse_;
|
||||||
std::map<CGDirectDisplayID, std::string> display_id_name_map_;
|
std::map<CGDirectDisplayID, std::string> display_id_name_map_;
|
||||||
unsigned char *nv12_frame_ = nullptr;
|
unsigned char *nv12_frame_ = nullptr;
|
||||||
|
size_t nv12_frame_size_ = 0;
|
||||||
int width_ = 0;
|
int width_ = 0;
|
||||||
int height_ = 0;
|
int height_ = 0;
|
||||||
int fps_ = 60;
|
int fps_ = 60;
|
||||||
@@ -100,7 +115,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
|||||||
// Helper object to receive Objective-C callbacks from ScreenCaptureKit and call into this C++
|
// Helper object to receive Objective-C callbacks from ScreenCaptureKit and call into this C++
|
||||||
// object. The helper may outlive this C++ instance, if a completion-handler is passed to
|
// object. The helper may outlive this C++ instance, if a completion-handler is passed to
|
||||||
// ScreenCaptureKit APIs and the C++ object is deleted before the handler executes.
|
// ScreenCaptureKit APIs and the C++ object is deleted before the handler executes.
|
||||||
SckHelper *__strong helper_;
|
SckHelper *__strong helper_ = nil;
|
||||||
// Callback for returning captured frames, or errors, to the caller. Only used on the caller's
|
// Callback for returning captured frames, or errors, to the caller. Only used on the caller's
|
||||||
// thread.
|
// thread.
|
||||||
cb_desktop_data _on_data = nullptr;
|
cb_desktop_data _on_data = nullptr;
|
||||||
@@ -110,7 +125,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
|||||||
// Guards some variables that may be accessed on different threads.
|
// Guards some variables that may be accessed on different threads.
|
||||||
std::mutex lock_;
|
std::mutex lock_;
|
||||||
// Provides captured desktop frames.
|
// Provides captured desktop frames.
|
||||||
SCStream *__strong stream_;
|
SCStream *__strong stream_ = nil;
|
||||||
// Currently selected display, or 0 if the full desktop is selected. This capturer does not
|
// Currently selected display, or 0 if the full desktop is selected. This capturer does not
|
||||||
// support full-desktop capture, and will fall back to the first display.
|
// support full-desktop capture, and will fall back to the first display.
|
||||||
CGDirectDisplayID current_display_ = 0;
|
CGDirectDisplayID current_display_ = 0;
|
||||||
@@ -182,6 +197,19 @@ ScreenCapturerSckImpl::ScreenCapturerSckImpl() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
|
ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
|
||||||
|
SckHelper *helper_to_release = nil;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(lock_);
|
||||||
|
if (stream_) {
|
||||||
|
[stream_ stopCaptureWithCompletionHandler:nil];
|
||||||
|
stream_ = nil;
|
||||||
|
}
|
||||||
|
_on_data = nullptr;
|
||||||
|
helper_to_release = helper_;
|
||||||
|
helper_ = nil;
|
||||||
|
}
|
||||||
|
[helper_to_release releaseCapturer];
|
||||||
|
|
||||||
display_info_list_.clear();
|
display_info_list_.clear();
|
||||||
display_id_map_.clear();
|
display_id_map_.clear();
|
||||||
display_id_map_reverse_.clear();
|
display_id_map_reverse_.clear();
|
||||||
@@ -190,15 +218,22 @@ ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
|
|||||||
if (nv12_frame_) {
|
if (nv12_frame_) {
|
||||||
delete[] nv12_frame_;
|
delete[] nv12_frame_;
|
||||||
nv12_frame_ = nullptr;
|
nv12_frame_ = nullptr;
|
||||||
|
nv12_frame_size_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[stream_ stopCaptureWithCompletionHandler:nil];
|
|
||||||
[helper_ releaseCapturer];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
||||||
|
if (!cb) {
|
||||||
|
LOG_ERROR("Screen capturer callback is null");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
_on_data = cb;
|
_on_data = cb;
|
||||||
fps_ = fps;
|
fps_ = fps > 0 ? fps : 60;
|
||||||
|
display_info_list_.clear();
|
||||||
|
display_id_map_.clear();
|
||||||
|
display_id_map_reverse_.clear();
|
||||||
|
display_id_name_map_.clear();
|
||||||
|
|
||||||
if (@available(macOS 10.15, *)) {
|
if (@available(macOS 10.15, *)) {
|
||||||
bool has_permission = CGPreflightScreenCaptureAccess();
|
bool has_permission = CGPreflightScreenCaptureAccess();
|
||||||
@@ -216,8 +251,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
|||||||
getShareableContentWithCompletionHandler:^(SCShareableContent *result, NSError *error) {
|
getShareableContentWithCompletionHandler:^(SCShareableContent *result, NSError *error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
capture_error = error;
|
capture_error = error;
|
||||||
LOG_ERROR("Failed to get shareable content: {}",
|
LOG_ERROR("Failed to get shareable content: {}", NSErrorToString(error));
|
||||||
std::string([error.localizedDescription UTF8String]));
|
|
||||||
} else {
|
} else {
|
||||||
content = result;
|
content = result;
|
||||||
}
|
}
|
||||||
@@ -227,7 +261,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
|||||||
|
|
||||||
if (capture_error || !content || content.displays.count == 0) {
|
if (capture_error || !content || content.displays.count == 0) {
|
||||||
LOG_ERROR("Failed to get display info, error: {}",
|
LOG_ERROR("Failed to get display info, error: {}",
|
||||||
std::string([capture_error.localizedDescription UTF8String]));
|
NSErrorToString(capture_error));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,51 +318,58 @@ int ScreenCapturerSckImpl::Start(bool show_cursor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int ScreenCapturerSckImpl::SwitchTo(int monitor_index) {
|
int ScreenCapturerSckImpl::SwitchTo(int monitor_index) {
|
||||||
if (stream_) {
|
auto display_it = display_id_map_.find(monitor_index);
|
||||||
[stream_ stopCaptureWithCompletionHandler:^(NSError *error) {
|
if (display_it == display_id_map_.end()) {
|
||||||
std::lock_guard<std::mutex> lock(lock_);
|
LOG_WARN("SwitchTo skipped, invalid monitor_index={}, displays={}",
|
||||||
stream_ = nil;
|
monitor_index, display_id_map_.size());
|
||||||
current_display_ = display_id_map_[monitor_index];
|
return -1;
|
||||||
StartOrReconfigureCapturer();
|
|
||||||
}];
|
|
||||||
} else {
|
|
||||||
current_display_ = display_id_map_[monitor_index];
|
|
||||||
StartOrReconfigureCapturer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CGDirectDisplayID target_display = display_it->second;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(lock_);
|
||||||
|
current_display_ = target_display;
|
||||||
|
}
|
||||||
|
StartOrReconfigureCapturer();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ScreenCapturerSckImpl::ResetToInitialMonitor() {
|
int ScreenCapturerSckImpl::ResetToInitialMonitor() {
|
||||||
int target = initial_monitor_index_;
|
int target = initial_monitor_index_;
|
||||||
if (display_info_list_.empty()) return -1;
|
if (display_info_list_.empty()) return -1;
|
||||||
CGDirectDisplayID target_display = display_id_map_[target];
|
auto display_it = display_id_map_.find(target);
|
||||||
if (current_display_ == target_display) return 0;
|
if (display_it == display_id_map_.end()) {
|
||||||
if (stream_) {
|
LOG_WARN("ResetToInitialMonitor skipped, invalid monitor_index={}", target);
|
||||||
[stream_ stopCaptureWithCompletionHandler:^(NSError *error) {
|
return -1;
|
||||||
std::lock_guard<std::mutex> lock(lock_);
|
|
||||||
stream_ = nil;
|
|
||||||
current_display_ = target_display;
|
|
||||||
StartOrReconfigureCapturer();
|
|
||||||
}];
|
|
||||||
} else {
|
|
||||||
current_display_ = target_display;
|
|
||||||
StartOrReconfigureCapturer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CGDirectDisplayID target_display = display_it->second;
|
||||||
|
if (current_display_ == target_display) return 0;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(lock_);
|
||||||
|
current_display_ = target_display;
|
||||||
|
}
|
||||||
|
StartOrReconfigureCapturer();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ScreenCapturerSckImpl::Destroy() {
|
int ScreenCapturerSckImpl::Destroy() {
|
||||||
std::lock_guard<std::mutex> lock(lock_);
|
SckHelper *helper_to_release = nil;
|
||||||
if (stream_) {
|
{
|
||||||
LOG_INFO("Destroying stream");
|
std::lock_guard<std::mutex> lock(lock_);
|
||||||
[stream_ stopCaptureWithCompletionHandler:nil];
|
if (stream_) {
|
||||||
stream_ = nil;
|
LOG_INFO("Destroying stream");
|
||||||
|
[stream_ stopCaptureWithCompletionHandler:nil];
|
||||||
|
stream_ = nil;
|
||||||
|
}
|
||||||
|
current_display_ = 0;
|
||||||
|
permanent_error_ = false;
|
||||||
|
_on_data = nullptr;
|
||||||
|
helper_to_release = helper_;
|
||||||
|
helper_ = nil;
|
||||||
}
|
}
|
||||||
current_display_ = 0;
|
|
||||||
permanent_error_ = false;
|
[helper_to_release releaseCapturer];
|
||||||
_on_data = nullptr;
|
|
||||||
[helper_ releaseCapturer];
|
|
||||||
helper_ = nil;
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -416,7 +457,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
|||||||
|
|
||||||
// TODO: crbug.com/327458809 - Choose an appropriate sampleHandlerQueue for
|
// TODO: crbug.com/327458809 - Choose an appropriate sampleHandlerQueue for
|
||||||
// best performance.
|
// best performance.
|
||||||
NSError *add_stream_output_error;
|
NSError *add_stream_output_error = nil;
|
||||||
dispatch_queue_t queue = dispatch_queue_create("ScreenCaptureKit.Queue", DISPATCH_QUEUE_SERIAL);
|
dispatch_queue_t queue = dispatch_queue_create("ScreenCaptureKit.Queue", DISPATCH_QUEUE_SERIAL);
|
||||||
bool add_stream_output_result = [stream_ addStreamOutput:helper_
|
bool add_stream_output_result = [stream_ addStreamOutput:helper_
|
||||||
type:SCStreamOutputTypeScreen
|
type:SCStreamOutputTypeScreen
|
||||||
@@ -425,7 +466,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
|||||||
|
|
||||||
if (!add_stream_output_result) {
|
if (!add_stream_output_result) {
|
||||||
stream_ = nil;
|
stream_ = nil;
|
||||||
LOG_ERROR("addStreamOutput failed");
|
LOG_ERROR("addStreamOutput failed: {}", NSErrorToString(add_stream_output_error));
|
||||||
permanent_error_ = true;
|
permanent_error_ = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -436,7 +477,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
|||||||
// calls stopCaptureWithCompletionHandler on the stream, which cancels
|
// calls stopCaptureWithCompletionHandler on the stream, which cancels
|
||||||
// this handler.
|
// this handler.
|
||||||
permanent_error_ = true;
|
permanent_error_ = true;
|
||||||
LOG_ERROR("startCaptureWithCompletionHandler failed");
|
LOG_ERROR("startCaptureWithCompletionHandler failed: {}", NSErrorToString(error));
|
||||||
} else {
|
} else {
|
||||||
LOG_INFO("Capture started");
|
LOG_INFO("Capture started");
|
||||||
}
|
}
|
||||||
@@ -448,8 +489,18 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
|||||||
|
|
||||||
void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
|
void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
|
||||||
CFDictionaryRef attachment) {
|
CFDictionaryRef attachment) {
|
||||||
|
(void)attachment;
|
||||||
|
if (!pixelBuffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
||||||
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
||||||
|
if (width == 0 || height == 0 || CVPixelBufferGetPlaneCount(pixelBuffer) < 2) {
|
||||||
|
LOG_ERROR("Invalid CVPixelBuffer: width={}, height={}, planes={}", width, height,
|
||||||
|
CVPixelBufferGetPlaneCount(pixelBuffer));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
CVReturn status = CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
CVReturn status = CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||||
if (status != kCVReturnSuccess) {
|
if (status != kCVReturnSuccess) {
|
||||||
@@ -458,18 +509,37 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
|
|||||||
}
|
}
|
||||||
|
|
||||||
size_t required_size = width * height * 3 / 2;
|
size_t required_size = width * height * 3 / 2;
|
||||||
if (!nv12_frame_ || (width_ * height_ * 3 / 2 < required_size)) {
|
if (required_size > static_cast<size_t>((std::numeric_limits<int>::max)())) {
|
||||||
|
LOG_ERROR("Captured frame is too large: {} bytes", required_size);
|
||||||
|
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(lock_);
|
||||||
|
if (!_on_data) {
|
||||||
|
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nv12_frame_ || nv12_frame_size_ < required_size) {
|
||||||
delete[] nv12_frame_;
|
delete[] nv12_frame_;
|
||||||
nv12_frame_ = new unsigned char[required_size];
|
nv12_frame_ = new unsigned char[required_size];
|
||||||
width_ = width;
|
nv12_frame_size_ = required_size;
|
||||||
height_ = height;
|
|
||||||
}
|
}
|
||||||
|
width_ = static_cast<int>(width);
|
||||||
|
height_ = static_cast<int>(height);
|
||||||
|
|
||||||
void *base_y = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
|
void *base_y = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
|
||||||
size_t stride_y = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
|
size_t stride_y = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
|
||||||
|
|
||||||
void *base_uv = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
|
void *base_uv = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
|
||||||
size_t stride_uv = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
|
size_t stride_uv = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
|
||||||
|
if (!base_y || !base_uv || stride_y < width || stride_uv < width) {
|
||||||
|
LOG_ERROR("Invalid CVPixelBuffer planes: base_y={}, base_uv={}, stride_y={}, stride_uv={}",
|
||||||
|
base_y != nullptr, base_uv != nullptr, stride_y, stride_uv);
|
||||||
|
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
unsigned char *dst_y = nv12_frame_;
|
unsigned char *dst_y = nv12_frame_;
|
||||||
for (size_t row = 0; row < height; ++row) {
|
for (size_t row = 0; row < height; ++row) {
|
||||||
@@ -481,7 +551,8 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
|
|||||||
memcpy(dst_uv + row * width, static_cast<unsigned char *>(base_uv) + row * stride_uv, width);
|
memcpy(dst_uv + row * width, static_cast<unsigned char *>(base_uv) + row * stride_uv, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
_on_data(nv12_frame_, width * height * 3 / 2, width, height,
|
_on_data(nv12_frame_, static_cast<int>(required_size), static_cast<int>(width),
|
||||||
|
static_cast<int>(height),
|
||||||
display_id_name_map_[current_display_].c_str());
|
display_id_name_map_[current_display_].c_str());
|
||||||
|
|
||||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||||
@@ -503,10 +574,14 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SckHelper *local_helper = helper_;
|
SckHelper *local_helper = helper_;
|
||||||
|
if (!local_helper) {
|
||||||
|
LOG_ERROR("Cannot reconfigure capturer: helper is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto handler = ^(SCShareableContent *content, NSError *error) {
|
auto handler = ^(SCShareableContent *content, NSError *error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
LOG_ERROR("getShareableContent failed: {}",
|
LOG_ERROR("getShareableContent failed: {}", NSErrorToString(error));
|
||||||
std::string([error.localizedDescription UTF8String]));
|
|
||||||
[local_helper onShareableContentCreated:nil];
|
[local_helper onShareableContentCreated:nil];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,17 @@ namespace crossdesk {
|
|||||||
class SpeakerCapturerMacosx;
|
class SpeakerCapturerMacosx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::string NSErrorToString(NSError* error) {
|
||||||
|
if (!error) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* description = [error.localizedDescription UTF8String];
|
||||||
|
return description ? description : "";
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
@interface SpeakerCaptureDelegate : NSObject <SCStreamDelegate, SCStreamOutput>
|
@interface SpeakerCaptureDelegate : NSObject <SCStreamDelegate, SCStreamOutput>
|
||||||
@property(nonatomic, assign) crossdesk::SpeakerCapturerMacosx* owner;
|
@property(nonatomic, assign) crossdesk::SpeakerCapturerMacosx* owner;
|
||||||
- (instancetype)initWithOwner:(crossdesk::SpeakerCapturerMacosx*)owner;
|
- (instancetype)initWithOwner:(crossdesk::SpeakerCapturerMacosx*)owner;
|
||||||
@@ -28,15 +39,36 @@ class SpeakerCapturerMacosx;
|
|||||||
ofType:(SCStreamOutputType)type {
|
ofType:(SCStreamOutputType)type {
|
||||||
if (type != SCStreamOutputTypeAudio) return;
|
if (type != SCStreamOutputTypeAudio) return;
|
||||||
|
|
||||||
|
crossdesk::SpeakerCapturerMacosx* owner = _owner;
|
||||||
|
if (!owner || !owner->cb_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
|
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
|
||||||
|
if (!blockBuffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
size_t length = CMBlockBufferGetDataLength(blockBuffer);
|
size_t length = CMBlockBufferGetDataLength(blockBuffer);
|
||||||
char* dataPtr = NULL;
|
char* dataPtr = NULL;
|
||||||
CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, NULL, &dataPtr);
|
OSStatus dataStatus =
|
||||||
|
CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, NULL, &dataPtr);
|
||||||
|
if (dataStatus != noErr || dataPtr == nullptr || length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
CMAudioFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
|
CMAudioFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
|
||||||
|
if (!formatDesc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const AudioStreamBasicDescription* asbd =
|
const AudioStreamBasicDescription* asbd =
|
||||||
CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc);
|
CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc);
|
||||||
|
if (!asbd || asbd->mChannelsPerFrame == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_owner->cb_ && dataPtr && length > 0 && asbd) {
|
if (owner->cb_) {
|
||||||
std::vector<short> out_pcm16;
|
std::vector<short> out_pcm16;
|
||||||
if (asbd->mFormatFlags & kAudioFormatFlagIsFloat) {
|
if (asbd->mFormatFlags & kAudioFormatFlagIsFloat) {
|
||||||
int channels = asbd->mChannelsPerFrame;
|
int channels = asbd->mChannelsPerFrame;
|
||||||
@@ -86,7 +118,10 @@ class SpeakerCapturerMacosx;
|
|||||||
size_t total_bytes = out_pcm16.size() * sizeof(short);
|
size_t total_bytes = out_pcm16.size() * sizeof(short);
|
||||||
unsigned char* p = (unsigned char*)out_pcm16.data();
|
unsigned char* p = (unsigned char*)out_pcm16.data();
|
||||||
for (size_t offset = 0; offset + frame_bytes <= total_bytes; offset += frame_bytes) {
|
for (size_t offset = 0; offset + frame_bytes <= total_bytes; offset += frame_bytes) {
|
||||||
_owner->cb_(p + offset, frame_bytes, "audio");
|
if (!owner->cb_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
owner->cb_(p + offset, frame_bytes, "audio");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,7 +190,7 @@ int SpeakerCapturerMacosx::Init(speaker_data_cb cb) {
|
|||||||
|
|
||||||
if (error || !impl_->content) {
|
if (error || !impl_->content) {
|
||||||
LOG_ERROR("Failed to get shareable content: {}",
|
LOG_ERROR("Failed to get shareable content: {}",
|
||||||
std::string([error.localizedDescription UTF8String]));
|
NSErrorToString(error));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +244,7 @@ int SpeakerCapturerMacosx::Start() {
|
|||||||
error:&addOutputError];
|
error:&addOutputError];
|
||||||
if (!ok || addOutputError) {
|
if (!ok || addOutputError) {
|
||||||
LOG_ERROR("addStreamOutput error: {}",
|
LOG_ERROR("addStreamOutput error: {}",
|
||||||
std::string([addOutputError.localizedDescription UTF8String]));
|
NSErrorToString(addOutputError));
|
||||||
impl_->stream = nil;
|
impl_->stream = nil;
|
||||||
impl_->delegate = nil;
|
impl_->delegate = nil;
|
||||||
return -1;
|
return -1;
|
||||||
@@ -220,7 +255,7 @@ int SpeakerCapturerMacosx::Start() {
|
|||||||
[impl_->stream startCaptureWithCompletionHandler:^(NSError* _Nullable error) {
|
[impl_->stream startCaptureWithCompletionHandler:^(NSError* _Nullable error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
LOG_ERROR("startCaptureWithCompletionHandler error: {}",
|
LOG_ERROR("startCaptureWithCompletionHandler error: {}",
|
||||||
std::string([error.localizedDescription UTF8String]));
|
NSErrorToString(error));
|
||||||
ret = -1;
|
ret = -1;
|
||||||
}
|
}
|
||||||
dispatch_semaphore_signal(semaStart);
|
dispatch_semaphore_signal(semaStart);
|
||||||
@@ -238,13 +273,14 @@ int SpeakerCapturerMacosx::Stop() {
|
|||||||
[impl_->stream stopCaptureWithCompletionHandler:^(NSError* error) {
|
[impl_->stream stopCaptureWithCompletionHandler:^(NSError* error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
LOG_ERROR("stopCaptureWithCompletionHandler error: {}",
|
LOG_ERROR("stopCaptureWithCompletionHandler error: {}",
|
||||||
std::string([error.localizedDescription UTF8String]));
|
NSErrorToString(error));
|
||||||
}
|
}
|
||||||
dispatch_semaphore_signal(sema);
|
dispatch_semaphore_signal(sema);
|
||||||
}];
|
}];
|
||||||
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
||||||
|
|
||||||
impl_->stream = nil;
|
impl_->stream = nil;
|
||||||
|
impl_->delegate.owner = nullptr;
|
||||||
impl_->delegate = nil;
|
impl_->delegate = nil;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user