fix: address native mac review feedback

This commit is contained in:
Etienne Lescot
2026-05-13 16:18:19 +02:00
parent 179047b834
commit e708ae973e
20 changed files with 149 additions and 69 deletions
+66 -40
View File
@@ -1,4 +1,5 @@
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process"; import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
import { EventEmitter } from "node:events";
import { constants as fsConstants } from "node:fs"; import { constants as fsConstants } from "node:fs";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
@@ -47,6 +48,7 @@ const RECORDING_FILE_PREFIX = "recording-";
const RECORDING_SESSION_SUFFIX = ".session.json"; const RECORDING_SESSION_SUFFIX = ".session.json";
const ALLOWED_IMPORT_VIDEO_EXTENSIONS = new Set([".webm", ".mp4", ".mov", ".avi", ".mkv"]); const ALLOWED_IMPORT_VIDEO_EXTENSIONS = new Set([".webm", ".mp4", ".mov", ".avi", ".mkv"]);
const PREVIEW_AUDIO_DIR = path.join(app.getPath("userData"), "preview-audio"); const PREVIEW_AUDIO_DIR = path.join(app.getPath("userData"), "preview-audio");
const nativeMacCaptureEvents = new EventEmitter();
/** /**
* Paths explicitly approved by the user via file picker dialogs or project loads. * Paths explicitly approved by the user via file picker dialogs or project loads.
@@ -996,6 +998,43 @@ function tryParseNativeHelperEvent(line: string) {
} }
} }
function inspectNativeMacCaptureOutput() {
for (const line of nativeMacCaptureOutput.split(/\r?\n/)) {
const event = tryParseNativeHelperEvent(line.trim());
if (event) {
nativeMacCaptureEvents.emit("helper-event", event);
}
}
}
function attachNativeMacCaptureOutputDrain(proc: ChildProcessWithoutNullStreams) {
let lineBuffer = "";
const drain = (chunk: Buffer) => {
const text = chunk.toString();
nativeMacCaptureOutput += text;
lineBuffer += text;
const lines = lineBuffer.split(/\r?\n/);
lineBuffer = lines.pop() ?? "";
for (const line of lines) {
const event = tryParseNativeHelperEvent(line.trim());
if (event) {
nativeMacCaptureEvents.emit("helper-event", event);
}
}
};
const cleanup = () => {
proc.stdout.off("data", drain);
proc.stderr.off("data", drain);
proc.off("close", cleanup);
proc.off("error", cleanup);
};
proc.stdout.on("data", drain);
proc.stderr.on("data", drain);
proc.once("close", cleanup);
proc.once("error", cleanup);
}
function waitForNativeMacCaptureStart(proc: ChildProcessWithoutNullStreams) { function waitForNativeMacCaptureStart(proc: ChildProcessWithoutNullStreams) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
@@ -1003,25 +1042,19 @@ function waitForNativeMacCaptureStart(proc: ChildProcessWithoutNullStreams) {
reject(new Error("Timed out waiting for native macOS capture to start")); reject(new Error("Timed out waiting for native macOS capture to start"));
}, 10_000); }, 10_000);
const inspect = (chunk: Buffer) => { const inspect = (event: Record<string, unknown>) => {
nativeMacCaptureOutput += chunk.toString(); if (event.event === "recording-started") {
for (const line of nativeMacCaptureOutput.split(/\r?\n/)) { cleanup();
const event = tryParseNativeHelperEvent(line.trim()); resolve();
if (!event) continue; return;
if (event.event === "recording-started") { }
cleanup(); if (event.event === "error") {
resolve(); cleanup();
return; reject(new Error(String(event.message ?? event.code ?? "Native macOS capture failed")));
}
if (event.event === "error") {
cleanup();
reject(new Error(event.message ?? event.code ?? "Native macOS capture failed"));
return;
}
} }
}; };
const onOutput = (chunk: Buffer) => inspect(chunk); const onOutput = (event: Record<string, unknown>) => inspect(event);
const onClose = (code: number | null) => { const onClose = (code: number | null) => {
cleanup(); cleanup();
reject( reject(
@@ -1037,16 +1070,15 @@ function waitForNativeMacCaptureStart(proc: ChildProcessWithoutNullStreams) {
}; };
const cleanup = () => { const cleanup = () => {
clearTimeout(timer); clearTimeout(timer);
proc.stdout.off("data", onOutput); nativeMacCaptureEvents.off("helper-event", onOutput);
proc.stderr.off("data", onOutput);
proc.off("close", onClose); proc.off("close", onClose);
proc.off("error", onError); proc.off("error", onError);
}; };
proc.stdout.on("data", onOutput); nativeMacCaptureEvents.on("helper-event", onOutput);
proc.stderr.on("data", onOutput);
proc.once("close", onClose); proc.once("close", onClose);
proc.once("error", onError); proc.once("error", onError);
inspectNativeMacCaptureOutput();
}); });
} }
@@ -1063,25 +1095,19 @@ function waitForNativeMacCaptureStop(proc: ChildProcessWithoutNullStreams) {
); );
}, 30_000); }, 30_000);
const inspect = (chunk: Buffer) => { const inspect = (event: Record<string, unknown>) => {
nativeMacCaptureOutput += chunk.toString(); if (event.event === "recording-stopped") {
for (const line of nativeMacCaptureOutput.split(/\r?\n/)) { cleanup();
const event = tryParseNativeHelperEvent(line.trim()); resolve(String(event.screenPath ?? nativeMacCaptureTargetPath ?? ""));
if (!event) continue; return;
if (event.event === "recording-stopped") { }
cleanup(); if (event.event === "error") {
resolve(event.screenPath ?? nativeMacCaptureTargetPath ?? ""); cleanup();
return; reject(new Error(String(event.message ?? event.code ?? "Native macOS capture failed")));
}
if (event.event === "error") {
cleanup();
reject(new Error(event.message ?? event.code ?? "Native macOS capture failed"));
return;
}
} }
}; };
const onOutput = (chunk: Buffer) => inspect(chunk); const onOutput = (event: Record<string, unknown>) => inspect(event);
const onClose = (code: number | null) => { const onClose = (code: number | null) => {
if (code === 0 && nativeMacCaptureTargetPath) { if (code === 0 && nativeMacCaptureTargetPath) {
cleanup(); cleanup();
@@ -1102,16 +1128,15 @@ function waitForNativeMacCaptureStop(proc: ChildProcessWithoutNullStreams) {
}; };
const cleanup = () => { const cleanup = () => {
clearTimeout(timer); clearTimeout(timer);
proc.stdout.off("data", onOutput); nativeMacCaptureEvents.off("helper-event", onOutput);
proc.stderr.off("data", onOutput);
proc.off("close", onClose); proc.off("close", onClose);
proc.off("error", onError); proc.off("error", onError);
}; };
proc.stdout.on("data", onOutput); nativeMacCaptureEvents.on("helper-event", onOutput);
proc.stderr.on("data", onOutput);
proc.once("close", onClose); proc.once("close", onClose);
proc.once("error", onError); proc.once("error", onError);
inspectNativeMacCaptureOutput();
}); });
} }
@@ -1722,6 +1747,7 @@ export function registerIpcHandlers(
stdio: ["pipe", "pipe", "pipe"], stdio: ["pipe", "pipe", "pipe"],
}); });
nativeMacCaptureProcess = proc; nativeMacCaptureProcess = proc;
attachNativeMacCaptureOutputDrain(proc);
await waitForNativeMacCaptureStart(proc); await waitForNativeMacCaptureStart(proc);
const captureStartedAtMs = Date.now(); const captureStartedAtMs = Date.now();
@@ -101,6 +101,10 @@ func parentElement(_ element: AXUIElement) -> AXUIElement? {
return nil return nil
} }
guard CFGetTypeID(value) == AXUIElementGetTypeID() else {
return nil
}
return (value as! AXUIElement) return (value as! AXUIElement)
} }
@@ -327,8 +327,8 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
AVCaptureDevice.requestAccess(for: .audio) { _ in AVCaptureDevice.requestAccess(for: .audio) { _ in
semaphore.signal() semaphore.signal()
} }
semaphore.wait() let waitResult = semaphore.wait(timeout: .now() + 30)
if AVCaptureDevice.authorizationStatus(for: .audio) != .authorized { if waitResult == .timedOut || AVCaptureDevice.authorizationStatus(for: .audio) != .authorized {
throw HelperError.permissionDenied("Microphone permission is required for native microphone capture.") throw HelperError.permissionDenied("Microphone permission is required for native microphone capture.")
} }
default: default:
+1 -1
View File
@@ -21,7 +21,7 @@
"i18n:check": "node scripts/i18n-check.mjs", "i18n:check": "node scripts/i18n-check.mjs",
"preview": "vite preview", "preview": "vite preview",
"build:native:mac": "node scripts/build-macos-screencapturekit-helper.mjs", "build:native:mac": "node scripts/build-macos-screencapturekit-helper.mjs",
"build:mac": "tsc && vite build && electron-builder --mac", "build:mac": "npm run build:native:mac && tsc && vite build && electron-builder --mac",
"build:native:win": "node scripts/build-windows-wgc-helper.mjs", "build:native:win": "node scripts/build-windows-wgc-helper.mjs",
"build:win": "npm run build:native:win && tsc && vite build && electron-builder --win --config.npmRebuild=false", "build:win": "npm run build:native:win && tsc && vite build && electron-builder --win --config.npmRebuild=false",
"build:linux": "tsc && vite build && electron-builder --linux AppImage deb pacman --config.npmRebuild=false", "build:linux": "tsc && vite build && electron-builder --linux AppImage deb pacman --config.npmRebuild=false",
@@ -70,6 +70,12 @@ if (result.status !== 0) {
fs.mkdirSync(buildDir, { recursive: true }); fs.mkdirSync(buildDir, { recursive: true });
fs.mkdirSync(distributableDir, { recursive: true }); fs.mkdirSync(distributableDir, { recursive: true });
for (const artifactPath of [builtHelperPath, builtCursorHelperPath]) {
if (!fs.existsSync(artifactPath)) {
console.error(`Swift build completed but expected artifact was not found: ${artifactPath}`);
process.exit(1);
}
}
fs.copyFileSync(builtHelperPath, localHelperPath); fs.copyFileSync(builtHelperPath, localHelperPath);
fs.copyFileSync(builtHelperPath, distributablePath); fs.copyFileSync(builtHelperPath, distributablePath);
fs.copyFileSync(builtCursorHelperPath, localCursorHelperPath); fs.copyFileSync(builtCursorHelperPath, localCursorHelperPath);
+9 -6
View File
@@ -282,21 +282,24 @@ export function LaunchWindow() {
return () => cancelAnimationFrame(id); return () => cancelAnimationFrame(id);
}, [isLanguageMenuOpen]); }, [isLanguageMenuOpen]);
const hudMouseEventsEnabledRef = useRef<boolean | undefined>(undefined);
const setHudMouseEventsEnabled = useCallback((enabled: boolean) => { const setHudMouseEventsEnabled = useCallback((enabled: boolean) => {
if (hudMouseEventsEnabledRef.current === enabled) {
return;
}
hudMouseEventsEnabledRef.current = enabled;
window.electronAPI?.setHudOverlayIgnoreMouseEvents?.(!enabled); window.electronAPI?.setHudOverlayIgnoreMouseEvents?.(!enabled);
}, []); }, []);
useEffect(() => { useEffect(() => {
window.electronAPI?.setHudOverlayIgnoreMouseEvents?.(true); setHudMouseEventsEnabled(false);
return () => { return () => {
window.electronAPI?.setHudOverlayIgnoreMouseEvents?.(false); window.electronAPI?.setHudOverlayIgnoreMouseEvents?.(false);
}; };
}, []); }, [setHudMouseEventsEnabled]);
useEffect(() => { useEffect(() => {
if (isLanguageMenuOpen) { setHudMouseEventsEnabled(isLanguageMenuOpen);
setHudMouseEventsEnabled(true);
}
}, [isLanguageMenuOpen, setHudMouseEventsEnabled]); }, [isLanguageMenuOpen, setHudMouseEventsEnabled]);
const [selectedSource, setSelectedSource] = useState("Screen"); const [selectedSource, setSelectedSource] = useState("Screen");
@@ -389,7 +392,7 @@ export function LaunchWindow() {
if (event.currentTarget.hasPointerCapture(event.pointerId)) { if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId); event.currentTarget.releasePointerCapture(event.pointerId);
} }
setHudMouseEventsEnabled(true); setHudMouseEventsEnabled(false);
}; };
return ( return (
+37 -4
View File
@@ -775,7 +775,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}, },
}, },
webcam: { webcam: {
enabled: false, enabled: webcamEnabled,
deviceId: webcamDeviceId, deviceId: webcamDeviceId,
deviceName: webcamDeviceName, deviceName: webcamDeviceName,
width: WEBCAM_TARGET_WIDTH, width: WEBCAM_TARGET_WIDTH,
@@ -857,6 +857,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}, 5000); }, 5000);
}); });
} }
if (!isCountdownRunActive(countdownRunToken)) {
return true;
}
if (webcamStream.current) { if (webcamStream.current) {
nativeWebcamRecorder = createRecorderHandle(webcamStream.current, { nativeWebcamRecorder = createRecorderHandle(webcamStream.current, {
mimeType: selectMimeType(), mimeType: selectMimeType(),
@@ -867,6 +870,12 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
setWebcamEnabledState(false); setWebcamEnabledState(false);
} }
} }
if (!isCountdownRunActive(countdownRunToken)) {
if (nativeWebcamRecorder && nativeWebcamRecorder.recorder.state !== "inactive") {
nativeWebcamRecorder.recorder.stop();
}
return true;
}
const request: NativeMacRecordingRequest = { const request: NativeMacRecordingRequest = {
schemaVersion: 1, schemaVersion: 1,
recordingId: activeRecordingId, recordingId: activeRecordingId,
@@ -916,6 +925,13 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
} }
throw new Error(result.error ?? "Native macOS capture failed."); throw new Error(result.error ?? "Native macOS capture failed.");
} }
if (!isCountdownRunActive(countdownRunToken)) {
if (nativeWebcamRecorder && nativeWebcamRecorder.recorder.state !== "inactive") {
nativeWebcamRecorder.recorder.stop();
}
await window.electronAPI.stopNativeMacRecording(true);
return true;
}
recordingId.current = result.recordingId; recordingId.current = result.recordingId;
nativeMacRecording.current = { nativeMacRecording.current = {
@@ -969,9 +985,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
if (platform === "darwin" && cursorCaptureMode === "editable-overlay") { if (platform === "darwin" && cursorCaptureMode === "editable-overlay") {
const access = await window.electronAPI.requestNativeMacCursorAccess(); const access = await window.electronAPI.requestNativeMacCursorAccess();
if (!access.granted) { if (!access.granted) {
toast.info( toast.info(t("recording.accessibilityAllowAndRetry"));
"Allow Accessibility access for OpenScreen, then press record again to start the countdown.",
);
return; return;
} }
} }
@@ -1415,6 +1429,18 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
} }
return; return;
} }
if (nativeMacRecording.current) {
const activeRecordingId = recordingId.current;
restarting.current = true;
discardRecordingId.current = activeRecordingId;
try {
await finalizeNativeMacRecording(true);
await startRecording();
} finally {
restarting.current = false;
}
return;
}
const activeScreenRecorder = screenRecorder.current; const activeScreenRecorder = screenRecorder.current;
if (!activeScreenRecorder || activeScreenRecorder.recorder.state === "inactive") return; if (!activeScreenRecorder || activeScreenRecorder.recorder.state === "inactive") return;
@@ -1480,6 +1506,13 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
void finalizeNativeWindowsRecording(true); void finalizeNativeWindowsRecording(true);
return; return;
} }
if (nativeMacRecording.current) {
const activeRecordingId = recordingId.current;
discardRecordingId.current = activeRecordingId;
allowAutoFinalize.current = false;
void finalizeNativeMacRecording(true);
return;
}
const activeScreenRecorder = screenRecorder.current; const activeScreenRecorder = screenRecorder.current;
if ( if (
+2 -1
View File
@@ -40,6 +40,7 @@
"cameraDenied": "تم رفض الوصول إلى الكاميرا. سيستمر التسجيل بدون كاميرا الويب.", "cameraDenied": "تم رفض الوصول إلى الكاميرا. سيستمر التسجيل بدون كاميرا الويب.",
"cameraDisconnected": "تم فصل كاميرا الويب.", "cameraDisconnected": "تم فصل كاميرا الويب.",
"cameraNotFound": "لم يتم العثور على كاميرا.", "cameraNotFound": "لم يتم العثور على كاميرا.",
"permissionDenied": "تم رفض إذن التسجيل. يرجى السماح بتسجيل الشاشة." "permissionDenied": "تم رفض إذن التسجيل. يرجى السماح بتسجيل الشاشة.",
"accessibilityAllowAndRetry": "اسمح بوصول تسهيلات الاستخدام لـ OpenScreen، ثم اضغط على التسجيل مرة أخرى لبدء العد التنازلي."
} }
} }
+2 -1
View File
@@ -40,6 +40,7 @@
"cameraDenied": "Camera access denied. Recording will continue without webcam.", "cameraDenied": "Camera access denied. Recording will continue without webcam.",
"cameraDisconnected": "Webcam disconnected.", "cameraDisconnected": "Webcam disconnected.",
"cameraNotFound": "Camera not found.", "cameraNotFound": "Camera not found.",
"permissionDenied": "Recording permission denied. Please allow screen recording." "permissionDenied": "Recording permission denied. Please allow screen recording.",
"accessibilityAllowAndRetry": "Allow Accessibility access for OpenScreen, then press record again to start the countdown."
} }
} }
+2 -1
View File
@@ -33,7 +33,8 @@
"cameraDenied": "Acceso a la cámara denegado. La grabación continuará sin cámara web.", "cameraDenied": "Acceso a la cámara denegado. La grabación continuará sin cámara web.",
"cameraDisconnected": "Cámara web desconectada.", "cameraDisconnected": "Cámara web desconectada.",
"cameraNotFound": "Cámara no encontrada.", "cameraNotFound": "Cámara no encontrada.",
"permissionDenied": "Permiso de grabación denegado. Por favor permite la grabación de pantalla." "permissionDenied": "Permiso de grabación denegado. Por favor permite la grabación de pantalla.",
"accessibilityAllowAndRetry": "Permite el acceso de accesibilidad para OpenScreen y luego pulsa grabar de nuevo para iniciar la cuenta atrás."
}, },
"loadingVideo": "Cargando video...", "loadingVideo": "Cargando video...",
"newRecording": { "newRecording": {
+2 -1
View File
@@ -39,7 +39,8 @@
"cameraDenied": "Accès à la caméra refusé. L'enregistrement continuera sans webcam.", "cameraDenied": "Accès à la caméra refusé. L'enregistrement continuera sans webcam.",
"cameraDisconnected": "Webcam déconnectée.", "cameraDisconnected": "Webcam déconnectée.",
"cameraNotFound": "Caméra introuvable.", "cameraNotFound": "Caméra introuvable.",
"permissionDenied": "Permission d'enregistrement refusée. Veuillez autoriser l'enregistrement d'écran." "permissionDenied": "Permission d'enregistrement refusée. Veuillez autoriser l'enregistrement d'écran.",
"accessibilityAllowAndRetry": "Autorisez l'accès Accessibilité pour OpenScreen, puis appuyez de nouveau sur enregistrer pour lancer le compte à rebours."
}, },
"loadingVideo": "Chargement de la vidéo..." "loadingVideo": "Chargement de la vidéo..."
} }
+2 -1
View File
@@ -40,6 +40,7 @@
"cameraDenied": "カメラのアクセスが拒否されました。ウェブカメラなしで録画を続行します。", "cameraDenied": "カメラのアクセスが拒否されました。ウェブカメラなしで録画を続行します。",
"permissionDenied": "録画の権限が拒否されました。画面録画を許可してください。", "permissionDenied": "録画の権限が拒否されました。画面録画を許可してください。",
"cameraDisconnected": "ウェブカメラが切断されました。", "cameraDisconnected": "ウェブカメラが切断されました。",
"cameraNotFound": "カメラが見つかりません。" "cameraNotFound": "カメラが見つかりません。",
"accessibilityAllowAndRetry": "OpenScreenにアクセシビリティアクセスを許可してから、もう一度録画を押してカウントダウンを開始してください。"
} }
} }
+2 -1
View File
@@ -40,6 +40,7 @@
"cameraDenied": "카메라 접근이 거부되었습니다. 웹캠 없이 녹화를 계속합니다.", "cameraDenied": "카메라 접근이 거부되었습니다. 웹캠 없이 녹화를 계속합니다.",
"permissionDenied": "녹화 권한이 거부되었습니다. 화면 녹화를 허용해 주세요.", "permissionDenied": "녹화 권한이 거부되었습니다. 화면 녹화를 허용해 주세요.",
"cameraDisconnected": "웹캠 연결이 끊어졌습니다.", "cameraDisconnected": "웹캠 연결이 끊어졌습니다.",
"cameraNotFound": "카메라를 찾을 수 없습니다." "cameraNotFound": "카메라를 찾을 수 없습니다.",
"accessibilityAllowAndRetry": "OpenScreen의 손쉬운 사용 접근을 허용한 다음, 카운트다운을 시작하려면 다시 녹화를 누르세요."
} }
} }
+2 -1
View File
@@ -40,6 +40,7 @@
"cameraDenied": "Доступ к камере запрещён. Запись продолжится без веб-камеры.", "cameraDenied": "Доступ к камере запрещён. Запись продолжится без веб-камеры.",
"cameraDisconnected": "Веб-камера отключена.", "cameraDisconnected": "Веб-камера отключена.",
"cameraNotFound": "Камера не найдена.", "cameraNotFound": "Камера не найдена.",
"permissionDenied": "Разрешение на запись запрещено. Пожалуйста, разрешите запись экрана." "permissionDenied": "Разрешение на запись запрещено. Пожалуйста, разрешите запись экрана.",
"accessibilityAllowAndRetry": "Разрешите OpenScreen доступ к Универсальному доступу, затем снова нажмите запись, чтобы начать обратный отсчет."
} }
} }
+2 -1
View File
@@ -33,7 +33,8 @@
"cameraDenied": "Kamera erişimi reddedildi. Kayıt kamera olmadan devam edecek.", "cameraDenied": "Kamera erişimi reddedildi. Kayıt kamera olmadan devam edecek.",
"permissionDenied": "Kayıt izni reddedildi. Lütfen ekran kaydına izin verin.", "permissionDenied": "Kayıt izni reddedildi. Lütfen ekran kaydına izin verin.",
"cameraDisconnected": "Webcam bağlantısı kesildi.", "cameraDisconnected": "Webcam bağlantısı kesildi.",
"cameraNotFound": "Kamera bulunamadı." "cameraNotFound": "Kamera bulunamadı.",
"accessibilityAllowAndRetry": "OpenScreen için Erişilebilirlik erişimine izin verin, ardından geri sayımı başlatmak için tekrar kayda basın."
}, },
"loadingVideo": "Video yükleniyor...", "loadingVideo": "Video yükleniyor...",
"newRecording": { "newRecording": {
+1 -2
View File
@@ -14,8 +14,7 @@
"disableSystemAudio": "Sistem sesini devre dışı bırak", "disableSystemAudio": "Sistem sesini devre dışı bırak",
"enableMicrophone": "Mikrofonu etkinleştir", "enableMicrophone": "Mikrofonu etkinleştir",
"disableMicrophone": "Mikrofonu devre dışı bırak", "disableMicrophone": "Mikrofonu devre dışı bırak",
"defaultMicrophone": "Varsayılan Mikrofon", "defaultMicrophone": "Varsayılan Mikrofon"
"nrLevel": {}
}, },
"webcam": { "webcam": {
"enableWebcam": "Kamerayı etkinleştir", "enableWebcam": "Kamerayı etkinleştir",
+1 -3
View File
@@ -194,7 +194,5 @@
"language": { "language": {
"title": "Dil" "title": "Dil"
}, },
"audio": { "audio": {}
"nrLevel": {}
}
} }
+2 -1
View File
@@ -40,6 +40,7 @@
"cameraDenied": "Quyền truy cập máy ảnh bị từ chối. Sẽ tiếp tục ghi hình không có webcam.", "cameraDenied": "Quyền truy cập máy ảnh bị từ chối. Sẽ tiếp tục ghi hình không có webcam.",
"cameraDisconnected": "Webcam bị ngắt kết nối.", "cameraDisconnected": "Webcam bị ngắt kết nối.",
"cameraNotFound": "Không tìm thấy máy ảnh.", "cameraNotFound": "Không tìm thấy máy ảnh.",
"permissionDenied": "Quyền ghi hình bị từ chối. Vui lòng cho phép ghi màn hình." "permissionDenied": "Quyền ghi hình bị từ chối. Vui lòng cho phép ghi màn hình.",
"accessibilityAllowAndRetry": "Cho phép OpenScreen truy cập Trợ năng, sau đó nhấn ghi lại để bắt đầu đếm ngược."
} }
} }
+2 -1
View File
@@ -40,6 +40,7 @@
"cameraDenied": "摄像头权限被拒绝。录制将继续,但不包含摄像头画面。", "cameraDenied": "摄像头权限被拒绝。录制将继续,但不包含摄像头画面。",
"cameraDisconnected": "摄像头已断开连接。", "cameraDisconnected": "摄像头已断开连接。",
"cameraNotFound": "未找到摄像头。", "cameraNotFound": "未找到摄像头。",
"permissionDenied": "录屏权限被拒绝。请允许屏幕录制。" "permissionDenied": "录屏权限被拒绝。请允许屏幕录制。",
"accessibilityAllowAndRetry": "允许 OpenScreen 使用辅助功能权限,然后再次按录制以开始倒计时。"
} }
} }
+2 -1
View File
@@ -40,6 +40,7 @@
"cameraDenied": "攝影機權限被拒絕。錄製將繼續,但不包含攝影機畫面。", "cameraDenied": "攝影機權限被拒絕。錄製將繼續,但不包含攝影機畫面。",
"permissionDenied": "錄影權限被拒絕。請允許螢幕錄製。", "permissionDenied": "錄影權限被拒絕。請允許螢幕錄製。",
"cameraDisconnected": "網路攝影機已中斷連線。", "cameraDisconnected": "網路攝影機已中斷連線。",
"cameraNotFound": "找不到攝影機。" "cameraNotFound": "找不到攝影機。",
"accessibilityAllowAndRetry": "允許 OpenScreen 使用輔助使用權限,然後再次按下錄製以開始倒數。"
} }
} }