Skip to content
Merged
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
316 changes: 21 additions & 295 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ specta = { version = "=2.0.0-rc.20", features = [
] }
serde = { version = "1", features = ["derive"] }

scap = { git = "https://github.com/CapSoftware/scap", rev = "3cefe71561ff" }
nokhwa = { git = "https://github.com/CapSoftware/nokhwa", rev = "b9c8079e82e2", features = [
"input-native",
"serialize",
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ tauri-plugin-deep-link = "2.2.0"
tauri-plugin-clipboard-manager = "2.2.1"
tauri-plugin-opener = "2.2.6"

scap = { workspace = true }
serde = { workspace = true }
serde_json = "1.0.111"
specta.workspace = true
Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/src-tauri/src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ fn play_audio(bytes: &'static [u8]) {
pub enum AppSounds {
StartRecording,
StopRecording,
Screenshot,
Notification,
}

Expand All @@ -32,7 +31,6 @@ impl AppSounds {
match self {
AppSounds::StartRecording => include_bytes!("../sounds/start-recording.ogg"),
AppSounds::StopRecording => include_bytes!("../sounds/stop-recording.ogg"),
AppSounds::Screenshot => include_bytes!("../sounds/screenshot.ogg"),
AppSounds::Notification => include_bytes!("../sounds/action.ogg"),
}
}
Expand Down
142 changes: 0 additions & 142 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,14 @@ use ffmpeg::ffi::AV_TIME_BASE;
use general_settings::GeneralSettingsStore;
use kameo::{Actor, actor::ActorRef};
use notifications::NotificationType;
use png::{ColorType, Encoder};
use recording::InProgressRecording;
use relative_path::RelativePathBuf;
use scap::{
capturer::Capturer,
frame::{Frame, VideoFrame},
};
use scap_targets::{Display, DisplayId, WindowId, bounds::LogicalBounds};
use serde::{Deserialize, Serialize};
use serde_json::json;
use specta::Type;
use std::{
collections::BTreeMap,
fs::File,
future::Future,
io::BufWriter,
marker::PhantomData,
path::{Path, PathBuf},
process::Command,
Expand Down Expand Up @@ -309,9 +301,6 @@ pub struct RequestStartRecording {
pub mode: RecordingMode,
}

#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)]
pub struct RequestNewScreenshot;

#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)]
pub struct RequestOpenRecordingPicker {
pub target_mode: Option<RecordingTargetMode>,
Expand Down Expand Up @@ -1250,135 +1239,6 @@ async fn upload_screenshot(
Ok(UploadResult::Success(share_link))
}

#[tauri::command]
#[specta::specta]
#[instrument(skip(app, _state))]
async fn take_screenshot(app: AppHandle, _state: MutableState<'_, App>) -> Result<(), String> {
let id = uuid::Uuid::new_v4().to_string();

let recording_dir = app
.path()
.app_data_dir()
.unwrap()
.join("screenshots")
.join(format!("{id}.cap"));

std::fs::create_dir_all(&recording_dir).map_err(|e| e.to_string())?;

let (width, height, bgra_data) = {
let options = scap::capturer::Options {
fps: 1,
output_type: scap::frame::FrameType::BGRAFrame,
show_highlight: false,
..Default::default()
};

if let Some(window) = CapWindowId::Main.get(&app) {
let _ = window.hide();
}

let mut capturer =
Capturer::build(options).map_err(|e| format!("Failed to construct error: {e}"))?;
capturer.start_capture();
let frame = capturer
.get_next_frame()
.map_err(|e| format!("Failed to get frame: {e}"))?;
capturer.stop_capture();

if let Some(window) = CapWindowId::Main.get(&app) {
let _ = window.show();
}

match frame {
Frame::Video(VideoFrame::BGRA(bgra_frame)) => Ok((
bgra_frame.width as u32,
bgra_frame.height as u32,
bgra_frame.data,
)),
_ => Err("Unexpected frame type".to_string()),
}
}?;

let now = chrono::Local::now();
let screenshot_name = format!(
"Cap {} at {}.png",
now.format("%Y-%m-%d"),
now.format("%H.%M.%S")
);
let screenshot_path = recording_dir.join(&screenshot_name);

let app_handle = app.clone();
let recording_dir = recording_dir.clone();
tokio::task::spawn_blocking(move || -> Result<(), String> {
let mut rgba_data = vec![0; bgra_data.len()];
for (bgra, rgba) in bgra_data.chunks_exact(4).zip(rgba_data.chunks_exact_mut(4)) {
rgba[0] = bgra[2];
rgba[1] = bgra[1];
rgba[2] = bgra[0];
rgba[3] = bgra[3];
}

let file = File::create(&screenshot_path).map_err(|e| e.to_string())?;
let w = &mut BufWriter::new(file);

let mut encoder = Encoder::new(w, width, height);
encoder.set_color(ColorType::Rgba);
encoder.set_compression(png::Compression::Fast);
let mut writer = encoder.write_header().map_err(|e| e.to_string())?;

writer
.write_image_data(&rgba_data)
.map_err(|e| e.to_string())?;

AppSounds::Screenshot.play();

let now = chrono::Local::now();
let screenshot_name = format!(
"Cap {} at {}.png",
now.format("%Y-%m-%d"),
now.format("%H.%M.%S")
);

use cap_project::*;
RecordingMeta {
platform: Some(Platform::default()),
project_path: recording_dir.clone(),
sharing: None,
pretty_name: screenshot_name,
inner: RecordingMetaInner::Studio(cap_project::StudioRecordingMeta::SingleSegment {
segment: cap_project::SingleSegment {
display: VideoMeta {
path: RelativePathBuf::from_path(
screenshot_path.strip_prefix(&recording_dir).unwrap(),
)
.unwrap(),
fps: 0,
start_time: None,
},
camera: None,
audio: None,
cursor: None,
},
}),
upload: None,
}
.save_for_project()
.unwrap();

NewScreenshotAdded {
path: screenshot_path,
}
.emit(&app_handle)
.ok();

Ok(())
})
.await
.map_err(|e| format!("Task join error: {e}"))??;

Ok(())
}

#[tauri::command]
#[specta::specta]
#[instrument(skip(app))]
Expand Down Expand Up @@ -1970,7 +1830,6 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
recording::list_windows_with_thumbnails,
windows::refresh_window_content_protection,
general_settings::get_default_excluded_windows,
take_screenshot,
list_audio_devices,
close_recordings_overlay_window,
fake_window::set_fake_window_bounds,
Expand Down Expand Up @@ -2050,7 +1909,6 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
RecordingStopped,
RequestStartRecording,
RequestOpenRecordingPicker,
RequestNewScreenshot,
RequestOpenSettings,
RequestScreenCapturePrewarm,
NewNotification,
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src-tauri/src/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ pub async fn request_permission(_permission: OSPermission) {

match _permission {
OSPermission::ScreenRecording => {
scap::request_permission();
#[cfg(target_os = "macos")]
scap_screencapturekit::request_permission();
}
OSPermission::Camera => {
thread::spawn(|| {
Expand Down Expand Up @@ -163,7 +164,7 @@ pub fn do_permissions_check(_initial_check: bool) -> OSPermissionsCheck {

OSPermissionsCheck {
screen_recording: {
let result = scap::has_permission();
let result = scap_screencapturekit::has_permission();
match (result, _initial_check) {
(true, _) => OSPermissionStatus::Granted,
(false, true) => OSPermissionStatus::Empty,
Expand Down
10 changes: 1 addition & 9 deletions apps/desktop/src-tauri/src/tray.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use crate::windows::ShowCapWindow;
use crate::{
RecordingStarted, RecordingStopped, RequestNewScreenshot, RequestOpenSettings, recording,
};
use crate::{RecordingStarted, RecordingStopped, RequestOpenSettings, recording};

use std::sync::{
Arc,
Expand All @@ -20,7 +18,6 @@ use tauri_specta::Event;

pub enum TrayItem {
OpenCap,
TakeScreenshot,
PreviousRecordings,
PreviousScreenshots,
OpenSettings,
Expand All @@ -32,7 +29,6 @@ impl From<TrayItem> for MenuId {
fn from(value: TrayItem) -> Self {
match value {
TrayItem::OpenCap => "open_cap",
TrayItem::TakeScreenshot => "take_screenshot",
TrayItem::PreviousRecordings => "previous_recordings",
TrayItem::PreviousScreenshots => "previous_screenshots",
TrayItem::OpenSettings => "open_settings",
Expand All @@ -49,7 +45,6 @@ impl TryFrom<MenuId> for TrayItem {
fn try_from(value: MenuId) -> Result<Self, Self::Error> {
match value.0.as_str() {
"open_cap" => Ok(TrayItem::OpenCap),
"take_screenshot" => Ok(TrayItem::TakeScreenshot),
"previous_recordings" => Ok(TrayItem::PreviousRecordings),
"previous_screenshots" => Ok(TrayItem::PreviousScreenshots),
"open_settings" => Ok(TrayItem::OpenSettings),
Expand Down Expand Up @@ -114,9 +109,6 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> {
.await;
});
}
Ok(TrayItem::TakeScreenshot) => {
let _ = RequestNewScreenshot.emit(&app_handle);
}
Ok(TrayItem::PreviousRecordings) => {
let _ = RequestOpenSettings {
page: "recordings".to_string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const ACTION_TEXT = {
startInstantRecording: "Start instant recording",
restartRecording: "Restart recording",
stopRecording: "Stop recording",
// takeScreenshot: "Take Screenshot",
openRecordingPicker: "Open recording picker",
openRecordingPickerDisplay: "Record display",
openRecordingPickerWindow: "Record window",
Expand Down
11 changes: 4 additions & 7 deletions apps/desktop/src/utils/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,6 @@ async refreshWindowContentProtection() : Promise<null> {
async getDefaultExcludedWindows() : Promise<WindowExclusion[]> {
return await TAURI_INVOKE("get_default_excluded_windows");
},
async takeScreenshot() : Promise<null> {
return await TAURI_INVOKE("take_screenshot");
},
async listAudioDevices() : Promise<string[]> {
return await TAURI_INVOKE("list_audio_devices");
},
Expand Down Expand Up @@ -346,7 +343,7 @@ export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall"
export type Audio = { duration: number; sample_rate: number; channels: number; start_time: number }
export type AudioConfiguration = { mute: boolean; improve: boolean; micVolumeDb?: number; micStereoMode?: StereoMode; systemVolumeDb?: number }
export type AudioInputLevelChange = number
export type AudioMeta = { path: string;
export type AudioMeta = { path: string;
/**
* unix time of the first frame
*/
Expand Down Expand Up @@ -398,11 +395,11 @@ export type Flags = { captions: boolean }
export type FramesRendered = { renderedCount: number; totalFrames: number; type: "FramesRendered" }
export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: boolean; hapticsEnabled?: boolean; autoCreateShareableLink?: boolean; enableNotifications?: boolean; disableAutoOpenLinks?: boolean; hasCompletedStartup?: boolean; theme?: AppTheme; commercialLicense?: CommercialLicense | null; lastVersion?: string | null; windowTransparency?: boolean; postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; custom_cursor_capture2?: boolean; serverUrl?: string; recordingCountdown?: number | null; enableNativeCameraPreview: boolean; autoZoomOnClicks?: boolean; enableNewRecordingFlow: boolean; postDeletionBehaviour?: PostDeletionBehaviour; excludedWindows?: WindowExclusion[]; deleteInstantRecordingsAfterUpload?: boolean; instantModeMaxResolution?: number }
export type GifExportSettings = { fps: number; resolution_base: XY<number>; quality: GifQuality | null }
export type GifQuality = {
export type GifQuality = {
/**
* Encoding quality from 1-100 (default: 90)
*/
quality: number | null;
quality: number | null;
/**
* Whether to prioritize speed over quality (default: false)
*/
Expand Down Expand Up @@ -480,7 +477,7 @@ export type UploadProgress = { progress: number }
export type UploadProgressEvent = { video_id: string; uploaded: string; total: string }
export type UploadResult = { Success: string } | "NotAuthenticated" | "PlanCheckFailed" | "UpgradeRequired"
export type Video = { duration: number; width: number; height: number; fps: number; start_time: number }
export type VideoMeta = { path: string; fps?: number;
export type VideoMeta = { path: string; fps?: number;
/**
* unix time of the first frame
*/
Expand Down
2 changes: 2 additions & 0 deletions crates/scap-screencapturekit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

mod capture;
mod config;
mod permission;

pub use capture::{AudioFrame, Capturer, CapturerBuilder, Frame, VideoFrame};
pub use config::StreamCfgBuilder;
pub use permission::{has_permission, request_permission};
21 changes: 21 additions & 0 deletions crates/scap-screencapturekit/src/permission.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// Requests screen capture access permission from the user.
///
/// On first call, this displays a system permission dialog. On subsequent calls,
/// it returns the current permission status without showing the dialog.
pub fn request_permission() -> bool {
unsafe { CGRequestScreenCaptureAccess() == 1 }
}

/// Checks whether screen capture access permission has been granted.
///
/// This is a non-blocking check that doesn't prompt the user.
pub fn has_permission() -> bool {
unsafe { (CGPreflightScreenCaptureAccess() & 1) == 1 }
}

#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
// Screen Capture Access
fn CGRequestScreenCaptureAccess() -> i32;
fn CGPreflightScreenCaptureAccess() -> i32;
}
Loading