Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/desktop-gpui/locales/de.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,10 @@ resuming = "Weiter…"
saving = "Sichert…"
starting = "Startet"

[controls.disconnected]
camera = "Kamera getrennt. Die Aufnahme läuft ohne Kamera-Overlay weiter."
microphone = "Mikrofon getrennt. Bis zur Wiederverbindung wird Stille aufgenommen."

[controls.marker]
add = "Marker hinzufügen"

Expand All @@ -1200,6 +1204,8 @@ mirror_title = "Spiegeln nicht verfügbar"
[camera.issue]
access_message = "Klicken Sie hier, um Cap in den Systemeinstellungen den Zugriff auf Ihre Kamera zu erlauben."
access_title = "Kamerazugriff erforderlich"
disconnected_message = "Die ausgewählte Kamera sendet kein Video mehr."
disconnected_title = "Kamera getrennt"
generic = "Die ausgewählte Kamera konnte nicht gestartet werden. Wählen Sie eine andere Kamera oder verbinden Sie diese erneut."
invalid_format = "Diese Kamera hat kein nutzbares Aufnahmeformat gemeldet."
not_found = "Diese Kamera ist nicht mehr verfügbar. Prüfen Sie, ob sie verbunden ist und in den Systemberechtigungen erlaubt ist."
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop-gpui/locales/en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,10 @@ resuming = { text = "Resuming…", max_px = 73, size = 14 }
saving = { text = "Saving…", max_px = 73, size = 14 }
starting = { text = "Starting", max_px = 73, size = 14 }

[controls.disconnected]
camera = "Camera disconnected. Recording continues without camera overlay."
microphone = "Microphone disconnected. Silence will be used until it reconnects."

[controls.marker]
add = "Add marker"

Expand All @@ -1202,6 +1206,8 @@ mirror_title = "Mirror unavailable"
[camera.issue]
access_message = "Click to allow Cap to use your camera in System Settings."
access_title = "Camera access needed"
disconnected_message = "The selected camera stopped sending video."
disconnected_title = "Camera disconnected"
generic = "The selected camera could not be started. Choose another camera or reconnect this one."
invalid_format = "This camera did not report a usable capture format."
not_found = "This camera is no longer available. Check that it is connected and allowed by system permissions."
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop-gpui/locales/es.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,10 @@ resuming = "Continúa…"
saving = "Guardar…"
starting = "Iniciando"

[controls.disconnected]
camera = "Cámara desconectada. La grabación continúa sin la cámara superpuesta."
microphone = "Micrófono desconectado. Se grabará silencio hasta que se vuelva a conectar."

[controls.marker]
add = "Añadir marcador"

Expand All @@ -1200,6 +1204,8 @@ mirror_title = "Espejo no disponible"
[camera.issue]
access_message = "Haz clic para permitir que Cap use tu cámara en Ajustes del Sistema."
access_title = "Se necesita acceso a la cámara"
disconnected_message = "La cámara seleccionada dejó de enviar video."
disconnected_title = "Cámara desconectada"
generic = "No se pudo iniciar la cámara seleccionada. Elige otra cámara o vuelve a conectar esta."
invalid_format = "Esta cámara no indicó ningún formato de captura utilizable."
not_found = "Esta cámara ya no está disponible. Comprueba que esté conectada y permitida en los permisos del sistema."
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop-gpui/locales/fr.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,10 @@ resuming = "Reprise…"
saving = "Arrêt…"
starting = "Début"

[controls.disconnected]
camera = "Caméra déconnectée. L'enregistrement continue sans l'incrustation de la caméra."
microphone = "Micro déconnecté. Du silence sera enregistré jusqu'à sa reconnexion."

[controls.marker]
add = "Ajouter un repère"

Expand All @@ -1200,6 +1204,8 @@ mirror_title = "Miroir indisponible"
[camera.issue]
access_message = "Cliquez pour autoriser Cap à utiliser votre caméra dans Réglages Système."
access_title = "Accès à la caméra requis"
disconnected_message = "La caméra sélectionnée n'envoie plus de vidéo."
disconnected_title = "Caméra déconnectée"
generic = "La caméra sélectionnée n'a pas pu démarrer. Choisissez une autre caméra ou reconnectez celle-ci."
invalid_format = "Cette caméra n'a signalé aucun format de capture utilisable."
not_found = "Cette caméra n'est plus disponible. Vérifiez qu'elle est connectée et autorisée dans les autorisations du système."
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop-gpui/locales/hi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,10 @@ resuming = "फिर शुरू…"
saving = "सहेज रहा…"
starting = "शुरू हो रहा"

[controls.disconnected]
camera = "कैमरा डिस्कनेक्ट हो गया। रिकॉर्डिंग कैमरा ओवरले के बिना जारी है।"
microphone = "माइक्रोफ़ोन डिस्कनेक्ट हो गया। दोबारा कनेक्ट होने तक साइलेंस रिकॉर्ड होगा।"

[controls.marker]
add = "मार्कर जोड़ें"

Expand All @@ -1200,6 +1204,8 @@ mirror_title = "मिरर उपलब्ध नहीं"
[camera.issue]
access_message = "सिस्टम सेटिंग्स में Cap को अपना कैमरा इस्तेमाल करने देने के लिए क्लिक करें।"
access_title = "कैमरा एक्सेस ज़रूरी है"
disconnected_message = "चुने गए कैमरे से वीडियो आना बंद हो गया।"
disconnected_title = "कैमरा डिस्कनेक्ट हो गया"
generic = "चुना गया कैमरा शुरू नहीं हो सका। कोई दूसरा कैमरा चुनें या इसे फिर से कनेक्ट करें।"
invalid_format = "इस कैमरे ने कोई इस्तेमाल लायक कैप्चर फ़ॉर्मैट नहीं बताया।"
not_found = "यह कैमरा अब उपलब्ध नहीं है। जाँचें कि यह कनेक्ट है और सिस्टम की अनुमतियों में इसकी अनुमति है।"
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop-gpui/locales/ja.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,10 @@ resuming = "再開中…"
saving = "保存中…"
starting = "開始中"

[controls.disconnected]
camera = "カメラが切断されました。カメラのオーバーレイなしで録画を続けます。"
microphone = "マイクが切断されました。再接続されるまで無音で録音します。"

[controls.marker]
add = "マーカーを追加"

Expand All @@ -1200,6 +1204,8 @@ mirror_title = "反転を使用できません"
[camera.issue]
access_message = "クリックして、システム設定でCapにカメラの使用を許可してください。"
access_title = "カメラへのアクセスが必要です"
disconnected_message = "選択したカメラから映像が届かなくなりました。"
disconnected_title = "カメラが切断されました"
generic = "選択したカメラを起動できませんでした。別のカメラを選ぶか、このカメラを接続し直してください。"
invalid_format = "このカメラから使用可能なキャプチャフォーマットを取得できませんでした。"
not_found = "このカメラは使用できなくなりました。接続されていて、システムの権限で許可されているか確認してください。"
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop-gpui/locales/zh-CN.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,10 @@ resuming = "恢复中…"
saving = "保存中…"
starting = "正在开始"

[controls.disconnected]
camera = "摄像头已断开。录制将在没有摄像头画面的情况下继续。"
microphone = "麦克风已断开。重新连接前将录制静音。"

[controls.marker]
add = "添加标记"

Expand All @@ -1200,6 +1204,8 @@ mirror_title = "镜像不可用"
[camera.issue]
access_message = "点击以在系统设置中允许 Cap 使用你的摄像头。"
access_title = "需要摄像头权限"
disconnected_message = "所选摄像头已停止发送视频。"
disconnected_title = "摄像头已断开"
generic = "无法启动所选摄像头。请选择其他摄像头,或重新连接此摄像头。"
invalid_format = "此摄像头未提供可用的捕获格式。"
not_found = "此摄像头已不可用。请检查它是否已连接,并已在系统权限中允许。"
Expand Down
44 changes: 34 additions & 10 deletions apps/desktop-gpui/src/camera_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,9 @@
//! Known deviations (also in the README): no 150ms opacity/translate
//! transitions on the toolbar and resize brackets (no animation pass in this
//! port), no `backdrop-blur-xs` behind the issue overlay (no per-element
//! backdrop blur hook in this gpui rev), no "Camera disconnected" overlay on
//! recording input loss (the Tauri `recordingEvent` InputLost seam has no
//! counterpart in this app yet), no `cursor-move` on the drag surface (gpui
//! has no 4-way move cursor; the resize handles do get their diagonal resize
//! cursors), and chrome state persists to `gpui-state.json` rather than
//! backdrop blur hook in this gpui rev), no `cursor-move` on the drag surface
//! (gpui has no 4-way move cursor; the resize handles do get their diagonal
//! resize cursors), and chrome state persists to `gpui-state.json` rather than
//! `localStorage`.

use std::sync::{
Expand Down Expand Up @@ -1093,8 +1091,12 @@ struct CameraPreviewView {
camera_error: Option<String>,
effect_issue: Option<EffectIssue>,
awaiting_access: bool,
/// The recording's camera was unplugged (`InputLost` in the Tauri
/// bubble, `camera.tsx:162-172`); cleared when it is back.
camera_disconnected: bool,
_feeds_subscription: Subscription,
_access_subscription: Subscription,
_session_subscription: Option<Subscription>,
}

impl CameraPreviewView {
Expand Down Expand Up @@ -1147,6 +1149,19 @@ impl CameraPreviewView {
cx.notify();
}
});
let session = crate::session::RecordingSession::try_global(cx);
let camera_disconnected = session
.as_ref()
.is_some_and(|session| session.read(cx).disconnected_inputs().camera);
let session_subscription = session.map(|session| {
cx.observe(&session, |this: &mut Self, session, cx| {
let disconnected = session.read(cx).disconnected_inputs().camera;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Camera disconnect warnings persist after recording stops

Medium Severity

Disconnect the camera during recording, stop before reconnecting it, then reconnect or select a working camera in the idle preview. RecordingSession::finish leaves input_watch.disconnected.camera set, the watcher exits outside Phase::Recording, and only the next recording activation resets it. Both this subscription and the preview initializer consume the stale flag without a phase check, so the disconnect overlay remains over healthy video. Clear recording-scoped disconnect state on completion and prevent late reconnect callbacks from restoring stale state, or gate the preview flag to a live recording.

Open in Capy

if this.camera_disconnected != disconnected {
this.camera_disconnected = disconnected;
cx.notify();
}
})
});
let feeds_subscription = cx.observe(&feeds, |this: &mut Self, feeds, cx| {
let awaiting = awaiting_camera_access(cx);
if this.awaiting_access != awaiting {
Expand Down Expand Up @@ -1194,8 +1209,10 @@ impl CameraPreviewView {
camera_error,
effect_issue: None,
awaiting_access,
camera_disconnected,
_feeds_subscription: feeds_subscription,
_access_subscription: access_subscription,
_session_subscription: session_subscription,
}
}

Expand Down Expand Up @@ -1285,8 +1302,10 @@ impl Render for CameraPreviewView {

let showing_frame = self.latest_frame.is_some() || self.retained.is_some();
let awaiting_access = self.awaiting_access && !showing_frame && self.camera_error.is_none();
let has_issue =
self.camera_error.is_some() || self.effect_issue.is_some() || awaiting_access;
let has_issue = self.camera_disconnected
|| self.camera_error.is_some()
|| self.effect_issue.is_some()
|| awaiting_access;

let loading = if !showing_frame {
self.loading_fade_started = None;
Expand Down Expand Up @@ -1424,9 +1443,14 @@ impl Render for CameraPreviewView {
.as_deref()
.is_some_and(|error| error.contains("access is unavailable"));
if let Some((title, message)) = self
.camera_error
.as_deref()
.map(camera_issue)
.camera_disconnected
.then(|| {
(
tr::camera::issue::disconnected_title(),
tr::camera::issue::disconnected_message(),
)
})
.or(self.camera_error.as_deref().map(camera_issue))
.or(self.effect_issue.map(EffectIssue::text))
.or(awaiting_access.then(|| {
(
Expand Down
24 changes: 20 additions & 4 deletions apps/desktop-gpui/src/controls_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ impl ControlsWindow {
session
.error
.clone()
.or_else(|| disconnected_issue(session.disconnected_inputs()))
.or_else(|| session.input_switch_error().map(str::to_owned))
.or_else(|| {
(session.paused_for_sleep && session.is_paused())
Expand Down Expand Up @@ -518,21 +519,22 @@ impl ControlsWindow {
let has_microphone = self.has_microphone
&& (session.mode() == Some(RecordingMode::Instant)
|| Feeds::global(cx).read(cx).microphone.is_some());
let disconnected = has_microphone && session.disconnected_inputs().microphone;
self.level_bar.update(cx, |bar, cx| {
bar.configure(
has_microphone && !muted,
has_microphone && !muted && !disconnected,
theme.blue_9.into(),
theme.gray_10.into(),
cx,
);
});

let icon = if !has_microphone || muted {
let icon = if !has_microphone || muted || disconnected {
"icons/mic-off.svg"
} else {
"icons/microphone.svg"
};
let icon_color: gpui::Hsla = if muted {
let icon_color: gpui::Hsla = if muted || disconnected {
theme.red_9.into()
} else if has_microphone {
theme.gray_12.into()
Expand Down Expand Up @@ -560,7 +562,7 @@ impl ControlsWindow {
}))
})
.child(svg().path(icon).size(px(20.)).text_color(icon_color))
.when(has_microphone && !muted, |this| {
.when(has_microphone && !muted && !disconnected, |this| {
this.child(self.level_bar.clone())
})
}
Expand Down Expand Up @@ -1213,6 +1215,20 @@ fn recording_issue_text(error: String) -> impl IntoElement + Styled {
)
}

/// `issueMessages` for lost inputs (`in-progress-recording.tsx:199-208`).
fn disconnected_issue(disconnected: crate::session::DisconnectedInputs) -> Option<String> {
let lines: Vec<&str> = [
disconnected
.microphone
.then(tr::controls::disconnected::microphone),
disconnected.camera.then(tr::controls::disconnected::camera),
]
.into_iter()
.flatten()
.collect();
(!lines.is_empty()).then(|| lines.join("\n"))
}

fn tick_delay(live: bool, elapsed: Duration) -> Duration {
if live {
until_next_second(elapsed)
Expand Down
18 changes: 18 additions & 0 deletions apps/desktop-gpui/src/feeds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,24 @@ impl Feeds {
cx.notify();
}

/// Opens the selected camera again on the same feed, for a camera that was
/// unplugged and is back. A feed a recording holds takes the same device
/// back without a new lock.
pub fn restart_camera(&mut self, cx: &mut Context<Self>) {
let Some(selection) = self.camera.clone() else {
return;
};
if self.camera_preview_parked {
return;
}
self.camera_epoch += 1;
self.camera_input_epoch
.store(self.camera_epoch, Ordering::Release);
self.camera_ready = None;
self.start_camera_preview(selection, cx);
cx.notify();
}

/// An Instant recording moved onto `label`, opened on a feed of its own.
/// Selects it (the main window persists that) and lets the app feed close
/// the microphone the recording left, so the level meter only hears the
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop-gpui/src/i18n/surface_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,22 @@ fn main_surfaces_match_the_literals_they_replaced() {
"Click to allow Cap to use your camera in System Settings.",
),
(tr::camera::issue::unavailable_title(), "Camera unavailable"),
(
tr::camera::issue::disconnected_title(),
"Camera disconnected",
),
(
tr::camera::issue::disconnected_message(),
"The selected camera stopped sending video.",
),
(
tr::controls::disconnected::microphone(),
"Microphone disconnected. Silence will be used until it reconnects.",
),
(
tr::controls::disconnected::camera(),
"Camera disconnected. Recording continues without camera overlay.",
),
(
tr::camera::issue::not_found(),
"This camera is no longer available. Check that it is connected and allowed by system permissions.",
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop-gpui/src/recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,15 @@ impl ActiveRecording {
/// Marks `at` in a Studio recording and returns the new marker count.
/// Instant recordings have no editor timeline to put a marker on, so
/// they take none.
/// The feed a microphone switch moved the recording onto, if any.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Method documentation describes another action

The new comment above switched_microphone_actor() repeats the method's name and leaves the existing marker documentation attached to the wrong method. AGENTS.md requires comments to explain non-obvious context rather than narrate the code. Remove the redundant line and move the marker documentation back to add_marker(). The new comments above WatchedInputs and disconnected_issue() also repeat their code. This repository requirement must be satisfied before merging.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/recording.rs
Line: 871

Comment:
**Method documentation describes another action**

The new comment above `switched_microphone_actor()` repeats the method's name and leaves the existing marker documentation attached to the wrong method. `AGENTS.md` requires comments to explain non-obvious context rather than narrate the code. Remove the redundant line and move the marker documentation back to `add_marker()`. The new comments above `WatchedInputs` and `disconnected_issue()` also repeat their code. This repository requirement must be satisfied before merging.

**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

pub(crate) fn switched_microphone_actor(&self) -> Option<ActorRef<MicrophoneFeed>> {
self.switched_microphone
.lock()
.unwrap()
.as_ref()
.map(|switched| switched._feed.0.clone())
}

pub(crate) fn studio_handle(&self) -> Option<studio_recording::ActorHandle> {
match &self.handle {
Handle::Studio(handle) => Some(handle.clone()),
Expand Down
Loading
Loading