Skip to content
Closed
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ specta = "=2.0.0-rc.19"
tauri-specta = { version = "=2.0.0-rc.14", features = ["derive", "typescript"] }
specta-typescript = "0.0.6"
dirs = "5.0.1"
rand = "0.8.5"

[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = [
Expand Down
12 changes: 11 additions & 1 deletion apps/desktop/src-tauri/capabilities/migrated.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@
"fs:allow-resource-read-recursive",
"fs:default",
"decorum:allow-show-snap-overlay",
"os:default"
"os:default",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "networkQuality",
"cmd": "networkQuality",
"args": [{ "validator": "\\S+" }]
}
]
}
]
}
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/gen/schemas/capabilities.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main","camera"],"permissions":["core:default","core:window:allow-close","core:window:allow-hide","core:window:allow-show","core:window:allow-center","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-maximize","core:window:allow-set-size","core:window:allow-set-focus","core:window:allow-start-dragging","core:window:allow-set-position","core:webview:allow-create-webview-window","shell:default","fs:allow-read-text-file","fs:allow-resource-read-recursive","fs:default","decorum:allow-show-snap-overlay","os:default"]}}
{"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main","camera"],"permissions":["core:default","core:window:allow-close","core:window:allow-hide","core:window:allow-show","core:window:allow-center","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-maximize","core:window:allow-set-size","core:window:allow-set-focus","core:window:allow-start-dragging","core:window:allow-set-position","core:webview:allow-create-webview-window","shell:default","fs:allow-read-text-file","fs:allow-resource-read-recursive","fs:default","decorum:allow-show-snap-overlay","os:default",{"identifier":"shell:allow-execute","allow":[{"args":[{"validator":"\\S+"}],"cmd":"networkQuality","name":"networkQuality"}]}]}}
3 changes: 3 additions & 0 deletions apps/desktop/src-tauri/src/media/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::{
},
time::{Duration, Instant},
};
use scap::capturer::Resolution;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, ChildStdin, Command};
use tokio::sync::Mutex;
Expand Down Expand Up @@ -84,6 +85,7 @@ impl MediaRecorder {
custom_device: Option<&str>,
max_screen_width: usize,
max_screen_height: usize,
video_resolution: Resolution,
) -> Result<(), String> {
if !scap::has_permission() {
tracing::warn!("Screen capturing permission not granted. Requesting permission...");
Expand All @@ -107,6 +109,7 @@ impl MediaRecorder {
let mut video_capturer = VideoCapturer::new(
max_screen_width,
max_screen_height,
video_resolution,
self.should_stop.clone(),
);
let adjusted_width = video_capturer.frame_width;
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src-tauri/src/media/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ pub struct VideoCapturer {
impl VideoCapturer {
pub const FPS: u32 = 30;

pub fn new(_width: usize, _height: usize, should_stop: SharedFlag) -> VideoCapturer {
pub fn new(_width: usize, _height: usize, resolution: Resolution, should_stop: SharedFlag) -> VideoCapturer {
let mut capturer = Capturer::new(Options {
fps: Self::FPS,
target: None,
show_cursor: true,
show_highlight: true,
excluded_targets: None,
output_type: FrameType::BGRAFrame,
output_resolution: Resolution::Captured,
output_resolution: resolution,
crop_area: None,
});

Expand Down Expand Up @@ -140,6 +140,7 @@ impl VideoCapturer {
options_clone,
screenshot_path.clone(),
RecordingAssetType::ScreenCapture,
None
));
match upload_task.await {
Ok(result) => match result {
Expand Down
109 changes: 103 additions & 6 deletions apps/desktop/src-tauri/src/recording.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
use futures::future::join_all;
use scap::capturer::Resolution as ScapResolution;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use tauri::State;
use tokio::sync::{oneshot, Mutex};
use tokio::time::Duration;
use rand::Rng;
use image::{DynamicImage, GenericImageView};

use crate::app::config;
use crate::upload::{upload_recording_asset, RecordingAssetType};
use crate::upload::{get_video_duration, upload_recording_asset, RecordingAssetType};

use crate::media::MediaRecorder;
use crate::utils::ffmpeg_path_as_str;

pub struct ActiveRecording {
pub media_process: MediaRecorder,
Expand Down Expand Up @@ -43,6 +48,7 @@ pub struct RecordingOptions {
pub audio_name: String,
pub aws_region: String,
pub aws_bucket: String,
pub video_resolution: String,
}

#[tauri::command]
Expand Down Expand Up @@ -130,7 +136,8 @@ pub async fn start_dual_recording(
#[specta::specta]
pub async fn stop_all_recordings(
state: State<'_, Arc<Mutex<RecordingState>>>,
) -> Result<(), String> {
is_validation_check: Option<bool>
) -> Result<String, String> {
let mut state = state.lock().await;

let Some(mut active_recording) = state.active_recording.take() else {
Expand All @@ -145,13 +152,13 @@ pub async fn stop_all_recordings(
.expect("Failed to stop media recording");

tracing::info!("Uploading stream.m3u8");
upload_recording_asset(
let upload_result = upload_recording_asset(
active_recording.recording_options,
state.data_dir.join("recording/stream.m3u8"),
RecordingAssetType::CombinedSourcePlaylist,
is_validation_check
)
.await
.ok();
.await;

active_recording.shutdown_flag.store(true, Ordering::SeqCst);

Expand All @@ -162,7 +169,7 @@ pub async fn stop_all_recordings(

tracing::info!("All recordings and uploads stopped.");

Ok(())
upload_result
}

fn clean_and_create_dir(dir: &Path) -> Result<(), String> {
Expand Down Expand Up @@ -219,6 +226,7 @@ async fn hls_upload_loop(
options,
file.path().to_owned(),
RecordingAssetType::CombinedSourceSegment,
None,
)
.await
.ok();
Expand Down Expand Up @@ -246,6 +254,16 @@ async fn prepare_media_recording(
max_screen_height: usize,
) -> Result<MediaRecorder, String> {
let mut media_recorder = MediaRecorder::new();
let video_resolution = match options.video_resolution.as_str() {
"480p" => ScapResolution::_480p,
"720p" => ScapResolution::_720p,
"1080p" => ScapResolution::_1080p,
"1440p" => ScapResolution::_1440p,
"2160p" => ScapResolution::_2160p,
"4320p" => ScapResolution::_4320p,
_ => ScapResolution::Captured,
};

media_recorder
.start_media_recording(
options.clone(),
Expand All @@ -254,7 +272,86 @@ async fn prepare_media_recording(
audio_name.as_ref().map(String::as_str),
max_screen_width,
max_screen_height,
video_resolution,
)
.await?;
Ok(media_recorder)
}

pub async fn validate_video_segment(file_path: &Path, num_frames: usize) -> Result<bool, String> {
let ffmpeg_binary_path_str = match ffmpeg_path_as_str() {
Ok(path) => path.to_owned(),
Err(_) => return Ok(false),
};
let duration = match get_video_duration(file_path) {
Ok(d) => d,
Err(_) => return Ok(false),
};
let mut rng = rand::thread_rng();
let mut valid_frames = 0;

for _ in 0..num_frames {
let random_time = rng.gen_range(0.0..duration);
let temp_path = format!(
"{}/frame_{}.png",
file_path.parent().unwrap().to_str().unwrap(),
random_time.to_string()
);
let output = match Command::new(&ffmpeg_binary_path_str)
.args(&[
"-ss",
&random_time.to_string(),
"-i",
file_path.to_str().unwrap(),
"-vframes",
"1",
"-f",
"image2",
"-vcodec",
"png",
&temp_path,
])
.output()
{
Ok(o) => o,
Err(_) => return Ok(false),
};

if !output.status.success() {
eprintln!("FFmpeg error: {}", String::from_utf8_lossy(&output.stderr));
continue;
}

let img = match image::open(&temp_path) {
Ok(i) => i,
Err(_) => return Ok(false),
};

if is_frame_valid(&img) {
valid_frames += 1;
}
if let Err(_) = std::fs::remove_file(&temp_path) {
return Ok(false);
}
}

let validity_ratio = valid_frames as f32 / num_frames as f32;
Ok(validity_ratio >= 0.8) // Consider the segment valid if at least 80% of frames are valid
}

fn is_frame_valid(img: &DynamicImage) -> bool {
let (width, height) = img.dimensions();
let total_pixels = width * height;
let mut non_black_pixels = 0;

for pixel in img.pixels() {
let [r, g, b, _] = pixel.2 .0;
if r > 10 || g > 10 || b > 10 {
non_black_pixels += 1;
}
}

let non_black_ratio = non_black_pixels as f32 / total_pixels as f32;
println!("non_black_ratio: {}", non_black_ratio);
non_black_ratio > 0.05 // Consider the frame valid if more than 5% of pixels are non-black
}
11 changes: 10 additions & 1 deletion apps/desktop/src-tauri/src/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::str;

use crate::recording::RecordingOptions;
use crate::recording::{validate_video_segment, RecordingOptions};
use crate::utils::ffmpeg_path_as_str;

#[derive(serde::Serialize)]
Expand Down Expand Up @@ -52,7 +52,16 @@ pub async fn upload_recording_asset(
options: RecordingOptions,
file_path: PathBuf,
file_type: RecordingAssetType,
is_validation_check: Option<bool>,
) -> Result<String, String> {
if is_validation_check.unwrap_or(false) {
let is_valid = validate_video_segment(&file_path, 10).await?;
if !is_valid {
return Err("ERR_INVALID_SEGMENT".to_string());
}
return Ok("VALID_SEGMENT".to_string());
}

tracing::info!("Uploading recording asset {file_type}...");

let file_name = file_path
Expand Down
20 changes: 20 additions & 0 deletions apps/desktop/src/components/icons/ArrowDown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react";
import { SVGProps } from "react";

export const ArrowDown = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={16}
height={16}
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2.5}
viewBox="0 0 24 24"
{...props}
>
<path d="m6 9 6 6 6-6" />
</svg>
);

19 changes: 19 additions & 0 deletions apps/desktop/src/components/icons/Resolution.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";
import { SVGProps } from "react";

export const ResolutionIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
{...props}
>
<path
d="m2.644 2.754 0.1 0c0.11 0 0.22 0 0.33 0l0.24 -0.001c0.219 -0.001 0.438 -0.001 0.658 0 0.237 0 0.473 0 0.71 -0.001q0.695 -0.001 1.39 -0.001 0.565 0 1.13 -0.001a3823.969 3823.969 0 0 1 3.38 -0.001h0.175c0.934 0 1.869 -0.001 2.803 -0.002q1.44 -0.002 2.879 -0.002c0.539 0 1.077 0 1.616 -0.001q0.688 -0.001 1.376 0c0.234 0 0.468 0 0.702 0q0.322 -0.001 0.643 0 0.116 0 0.232 -0.001c0.846 -0.005 1.558 0.212 2.181 0.805 0.579 0.594 0.825 1.265 0.823 2.084l0.001 0.171q0.001 0.234 0 0.469c0 0.169 0 0.338 0.001 0.506q0.001 0.495 0.001 0.991 0 0.403 0.001 0.806 0.001 1.144 0.001 2.287v0.249c0 0.666 0.001 1.332 0.002 1.998q0.002 1.027 0.002 2.054c0 0.384 0 0.768 0.001 1.152q0.001 0.491 0 0.981a74.25 74.25 0 0 0 0 0.5c0.001 0.181 0 0.362 0 0.544l0.002 0.157c-0.007 0.741 -0.298 1.393 -0.806 1.927 -0.509 0.497 -1.142 0.821 -1.863 0.822l-0.1 0c-0.11 0 -0.22 0 -0.33 0l-0.24 0.001c-0.219 0.001 -0.438 0.001 -0.658 0 -0.237 0 -0.473 0 -0.71 0.001q-0.695 0.001 -1.39 0.001 -0.565 0 -1.13 0.001 -1.603 0.001 -3.206 0.001h-0.349c-0.934 0 -1.869 0.001 -2.803 0.002q-1.44 0.002 -2.879 0.002c-0.539 0 -1.077 0 -1.616 0.001q-0.688 0.001 -1.376 0c-0.234 0 -0.468 0 -0.702 0q-0.322 0.001 -0.643 0 -0.116 0 -0.232 0.001c-0.846 0.005 -1.558 -0.212 -2.181 -0.805C0.231 19.86 -0.015 19.189 -0.013 18.37l-0.001 -0.171q-0.001 -0.234 0 -0.469c0 -0.169 0 -0.338 -0.001 -0.506q-0.001 -0.495 -0.001 -0.991 0 -0.403 -0.001 -0.806a1947.469 1947.469 0 0 1 -0.001 -2.537c0 -0.666 -0.001 -1.332 -0.002 -1.998a984.188 984.188 0 0 1 -0.002 -2.054c0 -0.384 0 -0.768 -0.001 -1.152q-0.001 -0.491 0 -0.981c0 -0.167 0 -0.333 0 -0.5 -0.001 -0.181 0 -0.362 0 -0.544l-0.002 -0.157c0.007 -0.741 0.298 -1.393 0.806 -1.927 0.509 -0.497 1.142 -0.821 1.863 -0.822M2.109 4.969c-0.262 0.34 -0.241 0.719 -0.241 1.128l0 0.159q0 0.264 0 0.527l0 0.378q-0.001 0.46 -0.001 0.92 0 0.374 0 0.748a3349.969 3349.969 0 0 0 -0.001 2.122v0.231q0 0.928 -0.001 1.855 -0.001 0.953 -0.001 1.906 0 0.535 -0.001 1.07 -0.001 0.455 0 0.911 0 0.232 0 0.464c0 0.168 0 0.336 0 0.504l-0.001 0.148c0.002 0.411 0.044 0.779 0.34 1.086 0.284 0.219 0.582 0.24 0.93 0.24l0.095 0c0.105 0 0.209 0 0.314 0l0.227 0q0.312 0 0.624 0c0.225 0 0.449 0 0.673 0q0.66 0.001 1.319 0.001 0.536 0 1.072 0 1.52 0.001 3.04 0.001h0.331q1.33 0 2.66 0.001 1.365 0.001 2.731 0.001 0.767 0 1.533 0.001 0.653 0.001 1.305 0 0.333 0 0.666 0 0.305 0.001 0.61 0 0.11 0 0.22 0c0.726 0.011 0.726 0.011 1.336 -0.34 0.262 -0.34 0.241 -0.719 0.241 -1.128l0 -0.159q0 -0.264 0 -0.527l0 -0.378q0.001 -0.46 0.001 -0.92 0 -0.374 0 -0.748 0.001 -1.061 0.001 -2.122v-0.231q0 -0.928 0.001 -1.855 0.001 -0.953 0.001 -1.906 0 -0.535 0.001 -1.07 0.001 -0.455 0 -0.911 0 -0.232 0 -0.464c0 -0.168 0 -0.336 0 -0.504l0.001 -0.148c-0.002 -0.411 -0.044 -0.779 -0.34 -1.086 -0.284 -0.219 -0.582 -0.24 -0.93 -0.24l-0.095 0c-0.105 0 -0.209 0 -0.314 0l-0.227 0q-0.312 0 -0.624 0 -0.337 0 -0.673 0 -0.66 -0.001 -1.319 -0.001 -0.536 0 -1.072 0 -1.52 -0.001 -3.04 -0.001h-0.331q-1.33 0 -2.66 -0.001 -1.365 -0.001 -2.731 -0.001 -0.767 0 -1.533 -0.001 -0.653 -0.001 -1.305 0 -0.333 0 -0.666 0 -0.305 -0.001 -0.61 0 -0.11 0 -0.22 0C2.719 4.617 2.719 4.617 2.109 4.969"
fill="#000000"
/>
<path d="M15.469 6.563h4.781v4.828h-1.875v-2.953h-2.906z" fill="#000000" />
<path d="M3.75 12.609h1.875v2.953h2.859v1.875H3.75z" fill="#000000" />
</svg>
);
Loading