Skip to content
9 changes: 6 additions & 3 deletions backend/sql/mock_data.sql
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ INSERT INTO
"avatar_url",
"created_at",
"updated_at",
"social_metadata"
"social_metadata",
"unsubscribe_id"
)
VALUES
(
Expand All @@ -42,7 +43,8 @@ VALUES
'https://tvline.com/wp-content/uploads/2011/04/greatscott_april27_514110427100239.jpg?w=514&h=360&crop=1',
NOW(),
NOW(),
null
null,
9969ac04-24a0-4fd4-9f22-2302406b1706
),
(
'0195013f-bf8a-706f-a4f0-11d87ef40fce',
Expand All @@ -55,6 +57,7 @@ VALUES
'https://www.myany.city/sites/default/files/styles/scaled_cropped_medium__260x260/public/field/image/node-related-images/sample-dwight-k-schrute.jpg?itok=8TfRscbA',
NOW(),
NOW(),
null
null,
9a5fc5f7-63a5-46ec-b50a-e4808d79e69f
) ON CONFLICT (id) DO NOTHING;

46,411 changes: 46,211 additions & 200 deletions backend/web/web-app-debug.html

Large diffs are not rendered by default.

46,411 changes: 46,211 additions & 200 deletions backend/web/web-app.html

Large diffs are not rendered by default.

51 changes: 47 additions & 4 deletions core/src/capture/capturer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use winit::{event_loop::EventLoopProxy, monitor::MonitorHandle};

use crate::{
utils::geometry::{aspect_fit, Extent},
UserEvent,
UserEvent, STREAM_FAILURE_EXIT_CODE,
};
use std::sync::{mpsc, Arc, Mutex};
use std::vec;
Expand All @@ -27,7 +27,6 @@ const SCREENSHOT_CAPTURE_SLEEP_MS: u64 = 33;
const MAX_SCREENSHOT_RETRY_ATTEMPTS: u32 = 100;
const MAX_STREAM_FAILURES_BEFORE_EXIT: u64 = 5;
const POLL_STREAM_TIMEOUT_SECS: u64 = 100;
const STREAM_FAILURE_EXIT_CODE: i32 = 2;
const POLL_STREAM_DATA_SLEEP_MS: u64 = 100;

#[cfg_attr(target_os = "windows", path = "windows.rs")]
Expand Down Expand Up @@ -56,6 +55,17 @@ pub enum CapturerError {
/// Common causes include:
#[error("Failed to capture frames")]
FailedToCaptureFrames,

/// Capture source list is empty.
///
/// This error could occur when the screen sharing engine fails from the os and
/// then we try to restart the stream.
#[error("Capture source list is empty")]
CaptureSourceListEmpty,

/// Couldn't find selected source.
#[error("Couldn't find selected source")]
SelectedSourceNotFound,
}

/// Platform-specific extensions for screen sharing and monitor management.
Expand Down Expand Up @@ -407,7 +417,7 @@ impl Capturer {
let scale = 1.0;
let mut stream = Stream::new(stream_resolution, scale, self.tx.clone(), include_cursor)?;

stream.start_capture(content.id);
stream.start_capture(content.id)?;
self.active_stream = Some(stream);
Ok(())
}
Expand Down Expand Up @@ -453,6 +463,8 @@ impl Capturer {
/// permanent capture errors are detected. Manual calls should be rare.
pub fn restart_stream(&mut self) {
log::info!("restart_stream");
std::thread::sleep(std::time::Duration::from_millis(200));

self.active_stream = match self.active_stream.take() {
Some(mut stream) => {
stream.stop_capture();
Expand All @@ -476,7 +488,38 @@ impl Capturer {
std::process::exit(STREAM_FAILURE_EXIT_CODE);
}
};
new_stream.start_capture(new_stream.source_id());

// Sometimes the capturer fails with a permanent error from the os.
// We can't really do much about it, as we are relying on the os
// and DesktopCapturer from libwebrtc for capturing the screen.
// So we just sleep and retry a few times in case it's a temporary error.
// If we can't restart the stream after 10 retries, we exit the process
// and inform the user to restart the application.
let mut res = new_stream.start_capture(new_stream.source_id());
for i in 0..10 {
if res.is_ok() {
break;
}

log::info!("restart_stream: Failed to start capture, retrying {i}/10 {res:?}");
std::thread::sleep(std::time::Duration::from_millis(100));

new_stream = match new_stream.copy() {
Ok(new_stream) => new_stream,
Err(_) => {
log::error!("restart_stream: Failed to copy stream");
sentry_utils::upload_logs_event("Stream copy failed".to_string());
std::process::exit(STREAM_FAILURE_EXIT_CODE);
}
};
res = new_stream.start_capture(new_stream.source_id());
}

if res.is_err() {
log::error!("restart_stream: Failed to start capture after 10 retries {res:?}");
sentry_utils::upload_logs_event("Stream start capture failed".to_string());
std::process::exit(STREAM_FAILURE_EXIT_CODE);
}

log::info!("restart_stream: new stream created");
Some(new_stream)
Expand Down
8 changes: 6 additions & 2 deletions core/src/capture/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,10 +374,13 @@ impl Stream {
/// # Notes
/// This method should only be called when the stream is not already capturing.
/// The capture thread will run until `stop_capture()` is called.
pub fn start_capture(&mut self, id: u32) {
pub fn start_capture(&mut self, id: u32) -> Result<(), CapturerError> {
log::info!("stream::start_capture: Starting capture for id: {id}");
let mut capturer = self.capturer.lock().unwrap();
let sources = capturer.get_source_list();
if sources.is_empty() {
return Err(CapturerError::CaptureSourceListEmpty);
}
let mut source = sources[0].clone();
for s in sources {
if s.id() == (id as u64) {
Expand All @@ -386,7 +389,7 @@ impl Stream {
}
}
if source.id() != (id as u64) {
log::warn!("start_capture: Source not found, capturing first source");
return Err(CapturerError::SelectedSourceNotFound);
}
self.source_id = id;
capturer.start_capture(source);
Expand All @@ -396,6 +399,7 @@ impl Stream {
run_capture_frame(rx, capturer_clone);
}));
self.tx = Some(tx);
Ok(())
}

/// Stops the capture process and terminates the worker thread.
Expand Down
1 change: 1 addition & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const SOCKET_MESSAGE_TIMEOUT_SECONDS: u64 = 30;

/// Process exit code for errors
const PROCESS_EXIT_CODE_ERROR: i32 = 1;
const STREAM_FAILURE_EXIT_CODE: i32 = 2;

#[derive(Error, Debug)]
pub enum ServerError {
Expand Down
15 changes: 14 additions & 1 deletion tauri/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ impl AppData {

/// Monitors core process output and emits crash events.
async fn show_stdout(mut receiver: Receiver<CommandEvent>, app_handle: AppHandle) {
let mut crash_msg = String::new();
while let Some(event) = receiver.recv().await {
match event {
CommandEvent::Stdout(line) => {
Expand All @@ -118,6 +119,18 @@ async fn show_stdout(mut receiver: Receiver<CommandEvent>, app_handle: AppHandle
}
CommandEvent::Terminated(payload) => {
log::error!("show_stdout: Terminated {payload:?}");
match payload.code {
Some(code) => {
if code == 1 {
crash_msg = "Core process terminated because it failed to receive messages from tauri".to_string();
} else if code == 2 {
crash_msg = "Core process terminated because capturing failed from the OS and couldn't be recovered".to_string();
}
}
None => {
crash_msg = "Core process terminated because of an unknown error. Please restart the app".to_string();
}
}
break;
}
CommandEvent::Error(e) => {
Expand All @@ -130,7 +143,7 @@ async fn show_stdout(mut receiver: Receiver<CommandEvent>, app_handle: AppHandle
log::info!("show_stdout: Finished");

// Communicate to the frontend that the core process has crashed.
let res = app_handle.emit("core_process_crashed", ());
let res = app_handle.emit("core_process_crashed", crash_msg);
if let Err(e) = res {
log::error!("Failed to emit core_process_crashed: {e:?}");
}
Expand Down
Binary file added tauri/src/assets/fail.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tauri/src/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const getAvailableTabs = (
key: "invite",
} as const,
{
label: "Report issue",
label: "Broken again?",
icon: <HiOutlineAnnotation className="size-4 stroke-[1.5]" />,
key: "report-issue",
} as const,
Expand Down
36 changes: 8 additions & 28 deletions tauri/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@ const buttonVariants = cva(
"border border-slate-200 bg-white shadow-xs hover:bg-slate-100 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50",
secondary:
"bg-slate-100 text-slate-900 shadow-xs hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50 dark:hover:bg-slate-800/80",
ghost:
"hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-50",
ghost: "hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-50",
link: "text-slate-900 underline-offset-4 hover:underline dark:text-slate-50",
hidden: "hidden",
loading:
"bg-slate-900 text-slate-50 shadow dark:bg-slate-50 dark:text-slate-900",
loading: "bg-slate-900 text-slate-50 shadow dark:bg-slate-50 dark:text-slate-900",
"gradient-white":
"btn-gradient-white text-black font-semibold text-xs shadow-[0px_2px_10px_rgba(0,0,0,0.07)] rounded-lg px-6 py-1.5 h-[29px] w-[75px] flex items-center justify-center border border-slate-300 border-opacity-50 hover:scale-[1.025] transition-all duration-300",
},
Expand All @@ -40,7 +38,7 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
}
},
);

export interface ButtonProps
Expand All @@ -51,43 +49,25 @@ export interface ButtonProps
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{ className, variant, size, asChild = false, isLoading = false, ...props },
ref
) => {
({ className, variant, size, asChild = false, isLoading = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
>
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props}>
{isLoading && (
<svg
className="animate-spin mr-2 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
></path>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
)}
{props.children}
</Comp>
);
}
},
);
Button.displayName = "Button";

Expand Down
Loading
Loading